Exception Handling
Available Exceptions as AppError¶
AppError is a sealed class hierarchy designed to represent various types of errors that can occur within the application. It extends Throwable, allowing it to be used in standard Kotlin error handling mechanisms.
The base AppError class takes a message, an optional cause (another Throwable), and an optional context (a lazy message provider).
Key subtypes include:
AppError.Composite: Represents a collection of multipleAppErrorinstances.AppError.Generic: For general errors not covered by more specific types.AppError.Internal: Base class for errors originating from within the application’s internal logic or infrastructure.
*ExternalService: Errors from external service integrations.
*NotImplemented: For features or operations that are not yet implemented.
*Implementation: Errors in the application’s own code logic.
*Infrastructure: Errors related to underlying infrastructure (e.g., database, network).
*IncompatibleState: When the application is in a state that’s incompatible with the requested operation.
*InternalService: Errors from other internal services.
*InternalTimeout: Timeouts when communicating with internal services.AppError.Invocation: Base class for errors related to the invocation of an operation, often due to invalid input or state.
*GeneralValidation: General validation failures.
*ContextValidation: Errors when the operational context is invalid.
*ArgumentValidation: Specific argument validation failures.
*NullArgument: When a required argument is null.
*NotFound: When a requested resource cannot be found.
*Duplicate: When an attempt is made to create a resource that already exists.AppError.Authorization: Base class for errors related to authorization and authentication.
*NotAuthenticated: When an operation requires an authenticated user but none is present.
*NotAuthorized: When the authenticated user does not have the necessary permissions.
Helper functions are available to convert standard exceptions into AppError instances:
Throwable.normalizeToAppError(): AppError: Converts common exceptions (likeJsonConvertException,BadRequestException,SerializationException,IllegalArgumentException) into anAppError.GeneralValidation. OtherThrowabletypes are wrapped in anAppError.Implementation.Throwable.normalize(messageTarget: String, context: LazyMessage? = null): AppError: Similar tonormalizeToAppError, but allows specifying amessageTargetfor more specific error reporting, typically resulting in anAppError.ArgumentValidationfor input-related exceptions.
Examples (id=exception-examples)¶
// Creating a specific AppError
val notFoundError = AppError.NotFound("UserResource", context = { "UserId: 123" })
// Using a helper to create an AppError
val todoError = TODOResult<Unit>("Implement user deletion") // Returns Result.failure(AppError.NotImplemented(...))
// Normalizing an existing exception
try {
// some operation that might throw IllegalArgumentException
throw IllegalArgumentException("Invalid age")
} catch (e: Throwable) {
val appError = e.normalize("age")
// appError will be AppError.ArgumentValidation("age", "Invalid age", null, e)
}
try {
// some operation that might throw an unknown exception
throw RuntimeException("Something unexpected happened")
} catch (e: Throwable) {
val appError = e.normalizeToAppError()
// appError will be AppError.Implementation("Unexpected Error: Something unexpected happened", null, e)
}
Normalizing Throwable instances to AppError¶
The common-module provides extension functions to convert standard Throwable instances into more specific AppError types. This is particularly useful for handling exceptions caught from libraries or external systems and translating them into the application’s error domain.
Throwable.normalizeToAppError(): AppError¶
This is the primary function for converting a generic Throwable into an AppError. Its behavior is as follows:
- If the
Throwableis an instance of:
*JsonConvertException
*BadRequestException
*SerializationException
*IllegalArgumentException
It is converted into anAppError.GeneralValidationwith the message “Unexpected InputError: [original message]”. A warning is logged. - For any other type of
Throwable, it is converted into anAppError.Implementationwith the message “Unexpected Error: [original message]”. An error is logged.
This function helps standardize error handling by ensuring that caught exceptions are mapped to the AppError hierarchy.
Example:
fun processInput(input: String): Result<Unit> {
return try {
if (input.isBlank()) {
throw IllegalArgumentException("Input cannot be blank")
}
// ... processing logic ...
Result.success(Unit)
} catch (e: Throwable) {
Result.failure(e.normalizeToAppError()) // Converts IllegalArgumentException to AppError.GeneralValidation
}
}
fun performOperation(): Result<Unit> {
return try {
// ... some operation that might throw an unexpected exception ...
throw RuntimeException("A critical system component failed")
} catch (e: Throwable) {
Result.failure(e.normalizeToAppError()) // Converts RuntimeException to AppError.Implementation
}
}
val blankInputResult = processInput("")
// blankInputResult will be Result.failure(AppError.GeneralValidation("Unexpected InputError: Input cannot be blank", ...))
val operationFailedResult = performOperation()
// operationFailedResult will be Result.failure(AppError.Implementation("Unexpected Error: A critical system component failed", ...))
Other Normalization Functions¶
Throwable.normalize(messageTarget: String, context: LazyMessage? = null): AppError:
This function offers more fine-grained control.
* If theThrowableis already anAppError, it’s returned as is.
* ForJsonConvertException,BadRequestException,SerializationException, orIllegalArgumentException, it’s converted toAppError.ArgumentValidation(messageTarget, this.message, context, this).
* OtherThrowabletypes are converted toAppError.Implementation("Unexpected Error for: $messageTarget", context, this).
This is useful when you can pinpoint the source or target of the error (e.g., a specific input field).
Throwable.normalizeExceptionToAppError(argumentName: String, validationMessage: String, context: LazyMessage? = null): Throwable(Deprecated):
This function is deprecated. Developers should migrate to usingnormalizeToAppError()for general cases ornormalize(messageTarget, ...)or direct construction ofAppErrorsubtypes (likeAppError.ArgumentValidation) for more specific scenarios.
It is generally recommended to use normalizeToAppError() for broad exception catching and normalize(messageTarget, ...) or direct AppError instantiation when more context about the error is available.
Collecting Validations¶
When multiple validation errors can occur, it’s useful to collect all of them rather than failing on the first one. AppError.Composite is designed for this purpose, allowing you to bundle multiple AppError instances into a single error.
Several extension functions on Iterable<Result<T>> facilitate the collection and handling of multiple results:
collectAll(): Result<List<T>>: Transforms an iterable ofResultobjects into a singleResultcontaining a list of all successful values. If any of the inputResultobjects is a failure,collectAllreturns the first encountered failure.collectAny(): Result<List<T>>: Transforms an iterable ofResultobjects into a singleResultcontaining a list of successful values. It collects all successes, even if there are failures. This function always returns aResult.successcontaining a list of the values from the successful results (the list might be empty if all results were failures).sieve(): Pair<List<T>, List<Throwable>>: Separates an iterable ofResultobjects into a pair of lists: one for successful values and one for theThrowableinstances from failed results.
Examples (id=composite-examples)¶
val result1: Result<Int> = Result.success(1)
val result2: Result<Int> = Result.failure(AppError.ArgumentValidation("inputB", "cannot be zero"))
val result3: Result<Int> = Result.success(3)
val result4: Result<Int> = Result.failure(AppError.GeneralValidation("Overall check failed"))
val allResults = listOf(result1, result2, result3, result4)
// Collect all successes, fail fast
val collectedAll = allResults.collectAll() // Result.failure(AppError.ArgumentValidation("inputB", "cannot be zero"))
// Collect any successes
val collectedAny = allResults.collectAny() // Result.success(listOf(1, 3))
// Sieve successes and failures
val (successes, failures) = allResults.sieve()
// successes will be listOf(1, 3)
// failures will be listOf(AppError.ArgumentValidation(...), AppError.GeneralValidation(...))
// If you need to create a composite error from the failures:
if (failures.isNotEmpty()) {
val compositeError = AppError.Composite(
message = "Multiple validation errors occurred",
causes = failures.map { it.normalize("validation") } // Normalize all throwables to AppError
)
// Handle compositeError
}
Functional Error Propagation: Result¶
The Result class is a core component for functional error handling, representing either a successful outcome (Result.success(value)) or a failure (Result.failure(throwable)). A suite of extension functions is provided to work with Result objects in a functional and concise way.
Key Result extension functions:
TODOResult<T>(msg: String = "Not Implemented"): Result<T>: A utility to quickly create aResult.failurewith anAppError.NotImplemented.unitify(): Result<Unit>: Converts aResult<*>to aResult<Unit>, preserving the success/failure status but discarding the original value.flatMap<T, R>(tr: (T) -> Result<R>): Result<R>: If theResultis a success, applies the transformation functiontr(which itself returns aResult) to the value. If it’s a failure, it propagates the failure.mapCatching<T, R>(tr: (T) -> R): Result<R>: If theResultis a success, applies the transformation functiontrto the value and wraps the operation in arunCatchingblock. This catches any exceptions thrown bytrand converts them into aResult.failure.flatten<T>(): Result<T>: Converts aResult<Result<T>>into aResult<T>.asyncFlatMap<T, R>(tr: suspend (T) -> Result<R>): Result<R>: The asynchronous version offlatMap, for use with suspending transformation functions.asyncMapCatching<T, R>(tr: suspend (T) -> R): Result<R>: The asynchronous version ofmapCatching, for use with suspending transformation functions that might throw exceptions.notNull<T>(err: AppError?): Result<T>: Converts aResult<T?>toResult<T>. If the original result was successful but contained anullvalue, it transforms it into aResult.failureusing the providederror a defaultAppError.NullArgument.orElse(other: Result<T>): Result<T>: If the primaryResultis a failure, this function returns theotherResult. If both are failures, it returns aResult.failurecontaining anAppError.Compositethat includes both original errors.
Examples (id=result-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 for further processing").let { Result.failure(it) }
}
return Result.success(data.length)
}
val validResult: Result<String> = processItem("item123")
val invalidResult: Result<String> = processItem("")
// flatMap
val flatMappedSuccess = validResult.flatMap { furtherProcessing(it) } // Result.success(16) (length of "Processed: item123")
val flatMappedFailure = invalidResult.flatMap { furtherProcessing(it) } // Result.failure(AppError.ArgumentValidation("id", "cannot be blank"))
val flatMappedFailure2 = validResult.flatMap { furtherProcessing(it.substring(0,5)) } // Result.failure(AppError.GeneralValidation("Data too short..."))
// mapCatching
val mappedSuccess = validResult.mapCatching { it.length } // Result.success(16)
val mappedFailure = invalidResult.mapCatching { it.length } // Result.failure(AppError.ArgumentValidation(...))
// Example of mapCatching with an exception in the transform
val exceptionInMap = Result.success("short").mapCatching {
if(it.length < 10) throw IllegalStateException("Too short!")
it.length
} // Result.failure(IllegalStateException("Too short!")) -> this will be wrapped by runCatching
// unitify
val unitified = validResult.unitify() // Result.success(Unit)
// flatten
val nestedResult: Result<Result<Int>> = validResult.map { furtherProcessing(it) }
val flattenedResult = nestedResult.flatten() // Same as flatMappedSuccess
// notNull
val nullableSuccess: Result<String?> = Result.success("hello")
val definitelyNotNull: Result<String> = nullableSuccess.notNull(AppError.Generic("Expected non-null string")) // Result.success("hello")
val nullSuccess: Result<String?> = Result.success(null)
val nowFailure: Result<String> = nullSuccess.notNull(AppError.Generic("Expected non-null string")) // Result.failure(AppError.Generic("Expected non-null string"))
val nowFailureDefault: Result<String> = nullSuccess.notNull(null) // Result.failure(AppError.NullArgument("value"))
// orElse
val primaryFailure = AppError.NotFound("ResourceA").let { Result.failure<String>(it) }
val secondarySuccess = Result.success("FallbackValue")
val secondaryFailure = AppError.NotImplemented("FallbackOperation").let { Result.failure<String>(it) }
val useFallback = primaryFailure.orElse(secondarySuccess) // Result.success("FallbackValue")
val compositeFailure = primaryFailure.orElse(secondaryFailure)
// compositeFailure will be Result.failure(AppError.Composite(...)) containing both NotFound and NotImplemented errors.
// TODOResult
val todo = TODOResult<Int>("Calculate complex value") // Result.failure(AppError.NotImplemented("Calculate complex value", ...))
// Async example (conceptual)
suspend fun asyncFetch(id: String): Result<String> = Result.success("data for $id")
suspend fun asyncProcess(data: String): Result<Int> = Result.success(data.length)
val asyncExample = Result.success("id1")
.asyncFlatMap { asyncFetch(it) } // Result.success("data for id1")
.asyncMapCatching { asyncProcess(it) } // Result.success(Result.success(12)) -> needs flatten or use asyncFlatMap for asyncProcess if it returns Result
// Corrected async chain:
.asyncFlatMap { asyncFetch(it) } // Result.success("data for id1")
.asyncFlatMap { asyncProcess(it) } // Result.success(12)
Copyright: © Arda Systems 2025, All rights reserved