Skip to content

Exception Handling

Arda uses a two-tier error handling strategy: the AppError sealed class hierarchy for domain-specific errors, and the Result<T> monad for functional error propagation.

AppError is a sealed class extending Throwable. It represents errors that can occur within the application. The base class takes a message, optional cause (another Throwable), and optional context (a lazy message provider).

AppError
├── AppError.Composite — collection of multiple AppError instances
├── AppError.Generic — general errors not covered by specific types
├── AppError.Internal — errors from internal logic or infrastructure
│ ├── ExternalService — external service integration errors
│ ├── NotImplemented — unimplemented features/operations
│ ├── Implementation — errors in application code logic
│ ├── Infrastructure — database, network, infrastructure errors
│ ├── IncompatibleState — state incompatible with requested operation
│ ├── InternalService — errors from other internal services
│ └── InternalTimeout — timeouts communicating with internal services
├── AppError.Invocation — invocation errors, often due to invalid input
│ ├── GeneralValidation — general validation failures
│ ├── ContextValidation — invalid operational context
│ ├── ArgumentValidation — specific argument validation failures
│ ├── NullArgument — required argument is null
│ ├── NotFound — requested resource not found
│ └── Duplicate — resource already exists
└── AppError.Authorization — authentication/authorization errors
├── NotAuthenticated — operation requires authenticated user
└── NotAuthorized — user lacks necessary permissions

Use the correct property names when constructing composite errors:

  • AppError.Composite → property is causes, not errors
  • AppError.ArgumentValidation → property is argumentName, not field

Avoid hardcoded field names in error messages — use column/property references to prevent stale strings after refactoring.

// Direct construction
val notFoundError = AppError.NotFound("UserResource", context = { "UserId: 123" })
// Placeholder for unimplemented operations
val todoError = TODOResult<Unit>("Implement user deletion")
// Returns Result.failure(AppError.NotImplemented(...))

Two extension functions convert standard Throwable instances to AppError:

The primary normalization function:

  • JsonConvertException, BadRequestException, SerializationException, IllegalArgumentExceptionAppError.GeneralValidation("Unexpected InputError: ...")
  • Any other ThrowableAppError.Implementation("Unexpected Error: ...")
fun processInput(input: String): Result<Unit> {
return try {
if (input.isBlank()) throw IllegalArgumentException("Input cannot be blank")
Result.success(Unit)
} catch (e: Throwable) {
Result.failure(e.normalizeToAppError()) // → AppError.GeneralValidation
}
}

Throwable.normalize(messageTarget: String, context: LazyMessage? = null): AppError

Section titled “Throwable.normalize(messageTarget: String, context: LazyMessage? = null): AppError”

Fine-grained normalization with a specific target:

  • If already an AppError, returned as-is
  • Input-related exceptions → AppError.ArgumentValidation(messageTarget, message, context, this)
  • Other exceptions → AppError.Implementation("Unexpected Error for: $messageTarget", context, this)
try {
throw IllegalArgumentException("Invalid age")
} catch (e: Throwable) {
val appError = e.normalize("age")
// → AppError.ArgumentValidation("age", "Invalid age", null, e)
}

Throwable.normalizeExceptionToAppError(...) is deprecated. Migrate to normalizeToAppError() or normalize(...).

When multiple validation errors can occur, AppError.Composite bundles them. Extension functions on Iterable<Result<T>>:

  • collectAll(): Result<List<T>> — all success or first failure (short-circuits on first error)
  • collectAny(): Result<List<T>> — collect all successes regardless of failures; always returns Result.success
  • sieve(): Pair<List<T>, List<Throwable>> — separates successes from failures
val allResults = listOf(result1, result2, result3, result4)
// Short-circuit on first failure
val collectedAll = allResults.collectAll()
// Collect any successes
val collectedAny = allResults.collectAny()
// Separate successes and failures
val (successes, failures) = allResults.sieve()
// Build a composite error from failures
if (failures.isNotEmpty()) {
val compositeError = AppError.Composite(
message = "Multiple validation errors occurred",
causes = failures.map { it.normalize("validation") }
)
}

Result<T> is the core type for functional error handling. Key extension functions:

FunctionDescription
TODOResult<T>(msg)Returns Result.failure(AppError.NotImplemented(...))
unitify()Converts Result<*> to Result<Unit>
flatMap<T,R>(tr)Chain operations returning Result; propagates failure
mapCatching<T,R>(tr)Wraps throwing code into Result
flatten<T>()Flattens Result<Result<T>> to Result<T>
asyncFlatMap<T,R>(tr)Async version of flatMap
asyncMapCatching<T,R>(tr)Async version of mapCatching
notNull<T>(err)Converts Result<T?> to Result<T>; failure if null
orElse(other)Return other if primary fails; Composite if both fail
fun processItem(id: String): Result<String> {
if (id.isBlank())
return AppError.ArgumentValidation("id", "cannot be blank").let { Result.failure(it) }
return Result.success("Processed: $id")
}
fun furtherProcessing(data: String): Result<Int> {
if (data.length < 10)
return AppError.GeneralValidation("Data too short").let { Result.failure(it) }
return Result.success(data.length)
}
// flatMap: chains Result-returning operations
val result = processItem("item123").flatMap { furtherProcessing(it) }
// mapCatching: wraps throwing code
val mapped = processItem("item123").mapCatching { it.length }
// notNull: fail if null value
val nonNull = Result.success("hello").notNull(AppError.Generic("Expected non-null"))
// orElse: fallback on failure
val fallback = AppError.NotFound("ResourceA").let { Result.failure<String>(it) }
.orElse(Result.success("FallbackValue")) // Result.success("FallbackValue")
// Async chain
val asyncResult = Result.success("id1")
.asyncFlatMap { asyncFetch(it) }
.asyncFlatMap { asyncProcess(it) }

See also Functional Programming for the broader ROP philosophy.