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 Hierarchy
Section titled “AppError Hierarchy”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 permissionsProperty Names
Section titled “Property Names”Use the correct property names when constructing composite errors:
AppError.Composite→ property iscauses, noterrorsAppError.ArgumentValidation→ property isargumentName, notfield
Avoid hardcoded field names in error messages — use column/property references to prevent stale strings after refactoring.
Creating AppError Instances
Section titled “Creating AppError Instances”// Direct constructionval notFoundError = AppError.NotFound("UserResource", context = { "UserId: 123" })
// Placeholder for unimplemented operationsval todoError = TODOResult<Unit>("Implement user deletion")// Returns Result.failure(AppError.NotImplemented(...))Normalizing Throwable to AppError
Section titled “Normalizing Throwable to AppError”Two extension functions convert standard Throwable instances to AppError:
Throwable.normalizeToAppError(): AppError
Section titled “Throwable.normalizeToAppError(): AppError”The primary normalization function:
JsonConvertException,BadRequestException,SerializationException,IllegalArgumentException→AppError.GeneralValidation("Unexpected InputError: ...")- Any other
Throwable→AppError.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 tonormalizeToAppError()ornormalize(...).
Collecting Validations
Section titled “Collecting Validations”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 returnsResult.successsieve(): Pair<List<T>, List<Throwable>>— separates successes from failures
val allResults = listOf(result1, result2, result3, result4)
// Short-circuit on first failureval collectedAll = allResults.collectAll()
// Collect any successesval collectedAny = allResults.collectAny()
// Separate successes and failuresval (successes, failures) = allResults.sieve()
// Build a composite error from failuresif (failures.isNotEmpty()) { val compositeError = AppError.Composite( message = "Multiple validation errors occurred", causes = failures.map { it.normalize("validation") } )}Functional Error Propagation: Result
Section titled “Functional Error Propagation: Result”Result<T> is the core type for functional error handling. Key extension functions:
| Function | Description |
|---|---|
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 |
Examples
Section titled “Examples”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 operationsval result = processItem("item123").flatMap { furtherProcessing(it) }
// mapCatching: wraps throwing codeval mapped = processItem("item123").mapCatching { it.length }
// notNull: fail if null valueval nonNull = Result.success("hello").notNull(AppError.Generic("Expected non-null"))
// orElse: fallback on failureval fallback = AppError.NotFound("ResourceA").let { Result.failure<String>(it) } .orElse(Result.success("FallbackValue")) // Result.success("FallbackValue")
// Async chainval asyncResult = Result.success("id1") .asyncFlatMap { asyncFetch(it) } .asyncFlatMap { asyncProcess(it) }See also Functional Programming for the broader ROP philosophy.
Copyright: © Arda Systems 2025-2026, All rights reserved