kotlinintermediate

Kotlin Error Handling Patterns

Comprehensive error handling: sealed result types, validated aggregation, and railway-oriented programming.

kotlin
// Validated: collect ALL errors instead of failing on first
data class Validated<out E, out A>(
    val value: A?,
    val errors: List<E>
) {
    val isValid get() = errors.isEmpty()

    companion object {
        fun <E, A> valid(value: A) = Validated<E, A>(value, emptyList())
        fun <E, A> invalid(errors: List<E>) = Validated<E, A>(null, errors)
        fun <E, A> invalid(error: E) = Validated<E, A>(null, listOf(error))
    }
}

fun <E, A, B> Validated<E, A>.map(f: (A) -> B): Validated<E, B> =
    if (isValid) Validated.valid(f(value!!)) else Validated.invalid(errors)

// Combine multiple validations
fun <E, A, B, C> zip(
    v1: Validated<E, A>,
    v2: Validated<E, B>,
    f: (A, B) -> C
): Validated<E, C> {
    val allErrors = v1.errors + v2.errors
    return if (allErrors.isEmpty()) Validated.valid(f(v1.value!!, v2.value!!))
    else Validated.invalid(allErrors)
}

// Validation rules
typealias ValidationError = String

fun validateName(name: String): Validated<ValidationError, String> =
    if (name.isNotBlank() && name.length in 2..50)
        Validated.valid(name.trim())
    else Validated.invalid("Name must be 2-50 characters")

fun validateEmail(email: String): Validated<ValidationError, String> =
    if (email.matches(Regex(".+@.+\\..+")))
        Validated.valid(email.lowercase().trim())
    else Validated.invalid("Invalid email format")

fun validateAge(age: Int): Validated<ValidationError, Int> {
    val errors = mutableListOf<ValidationError>()
    if (age < 0) errors.add("Age must be positive")
    if (age > 150) errors.add("Age must be realistic")
    return if (errors.isEmpty()) Validated.valid(age)
    else Validated.invalid(errors)
}

data class UserRegistration(val name: String, val email: String, val age: Int)

fun validateRegistration(
    name: String, email: String, age: Int
): Validated<ValidationError, UserRegistration> {
    val vName = validateName(name)
    val vEmail = validateEmail(email)
    val vAge = validateAge(age)
    val allErrors = vName.errors + vEmail.errors + vAge.errors
    return if (allErrors.isEmpty())
        Validated.valid(UserRegistration(vName.value!!, vEmail.value!!, vAge.value!!))
    else Validated.invalid(allErrors)
}

// Kotlin built-in Result chaining
fun parseAndValidate(input: Map<String, String>): Result<UserRegistration> = runCatching {
    val name = requireNotNull(input["name"]) { "name is required" }
    val email = requireNotNull(input["email"]) { "email is required" }
    val age = requireNotNull(input["age"]?.toIntOrNull()) { "valid age is required" }
    require(name.isNotBlank()) { "name must not be blank" }
    require(email.contains("@")) { "invalid email" }
    require(age in 0..150) { "invalid age" }
    UserRegistration(name, email, age)
}

fun main() {
    // Validated: collects all errors
    val valid = validateRegistration("Alice", "alice@test.com", 30)
    println("Valid: $valid")

    val invalid = validateRegistration("", "bad-email", -5)
    println("Invalid: $invalid")
    println("Errors: ${invalid.errors}")

    // Result chaining
    val good = parseAndValidate(mapOf("name" to "Bob", "email" to "bob@t.com", "age" to "25"))
    val bad = parseAndValidate(mapOf("email" to "nope"))
    println("Good: ${good.getOrNull()}")
    println("Bad: ${bad.exceptionOrNull()?.message}")

    // runCatching chain
    val result = runCatching { "42" }
        .mapCatching { it.toInt() }
        .mapCatching { require(it > 0) { "Must be positive" }; it }
        .map { it * 2 }
        .getOrElse { 0 }
    println("Chained: $result")
}

Use Cases

  • Form validation with error accumulation
  • Input parsing with comprehensive error messages
  • Railway-oriented business logic

Tags

Related Snippets

Similar patterns you can reuse in the same workflow.