
Comprehensive error-handling toolkit offering rich, actionable exceptions with UI metadata, centralized internationalized error catalog, and composable UI components for consistent, severity-aware error presentation.
A comprehensive Kotlin Multiplatform error-handling library that provides a scalable, type-safe approach to managing exceptions with rich UI presentation metadata and centralized error messaging.
Author & Maintainer: Emmanuel Conradie
Arrow Errors helps you build consistent, user-friendly error experiences across your Kotlin Multiplatform applications. Born from the need to standardize error handling across platforms, this library transforms the way you think about exceptions—from simple error messages to rich, actionable user experiences.
Instead of throwing generic exceptions with plain string messages, Arrow Errors enables you to throw rich, actionable exceptions that carry all the information needed to present meaningful error dialogs to users. With built-in internationalization support, centralized error messaging, and ready-to-use UI components, you can create professional error experiences with minimal boilerplate.
The library consists of three complementary modules that work independently or together:
Whether you're building an Android app, iOS application, or desktop software, Arrow Errors provides a unified approach to error handling that scales with your project.
Arrow Errors is published to Maven Central. Add the dependencies you need to your Kotlin Multiplatform project.
Latest Version: Check the releases page for the current version.
For the complete error-handling solution with UI components:
// In your build.gradle.kts
// Replace $version with the latest version from releases
dependencies {
implementation("io.github.blackarrows-apps:arrow-errors-core:$version")
implementation("io.github.blackarrows-apps:arrow-errors-catalog:$version")
implementation("io.github.blackarrows-apps:arrow-errors-compose:$version")
}Choose only what you need:
error-core - Core error handling infrastructure:
dependencies {
implementation("io.github.blackarrows-apps:arrow-errors-core:$version")
}error-catalog - Centralized error messages with i18n:
dependencies {
implementation("io.github.blackarrows-apps:arrow-errors-catalog:$version")
}error-compose - Compose Multiplatform UI components:
dependencies {
implementation("io.github.blackarrows-apps:arrow-errors-compose:$version")
}Want to see Arrow Errors in action? Install the interactive Android playground:
./gradlew :sample:installDebugThe sample app demonstrates all error types, presentations, and i18n features with a comprehensive UI playground. See the sample README for more details.
Get up and running in minutes with these examples:
// Throw a rich, actionable exception
throw NetworkException(
id = "network_error",
msg = UiMessage.Plain("Unable to connect. Please check your internet connection."),
severity = ErrorSeverity.Error,
presentation = ErrorPresentation.Snackbar,
primaryAction = ErrorAction.Retry
)
// Handle in UI
try {
fetchData()
} catch (e: ActionableException) {
when (e.presentation) {
ErrorPresentation.Snackbar -> showSnackbar(e.msg)
ErrorPresentation.Dialog -> showDialog(e)
ErrorPresentation.FullScreen -> showErrorScreen(e)
ErrorPresentation.Silent -> logError(e)
}
}import io.blackarrows.errors.catalog.i18n.DefaultMessageResolver
import io.blackarrows.errors.catalog.factories.*
// Set locale once during app initialization
DefaultMessageResolver.setLocale("es") // Spanish
// Use exception factory functions (messages automatically in Spanish)
throw networkException(
error = IOException("Connection failed")
)
// Message: "La red no está disponible. Por favor, verifica tu conexión."@Composable
fun MyScreen(viewModel: MyViewModel) {
val error by viewModel.error.collectAsState()
Scaffold { padding ->
MyScreenContent(modifier = Modifier.padding(padding))
// ErrorPresenter automatically displays the right UI
ErrorPresenter(
error = error,
onDismiss = { viewModel.clearError() },
onActionClick = { actionId ->
when (actionId) {
"retry" -> viewModel.retry()
"dismiss" -> viewModel.clearError()
}
},
onNavigate = { navigation ->
when (navigation) {
ErrorNavigation.Login -> navController.navigate("login")
ErrorNavigation.Back -> navController.popBackStack()
else -> {}
}
}
)
}
}The base exception class that all errors extend. It includes:
open class ActionableException(
open val id: String? = null, // Unique ID for deduplication/logging
open val msg: UiMessage = ..., // User-facing message
open val severity: ErrorSeverity = ..., // Info, Warning, Error, Critical
open val presentation: ErrorPresentation = ..., // Snackbar, Dialog, FullScreen, Silent
open val primaryAction: ErrorAction? = null, // Primary button action
open val secondaryAction: ErrorAction? = null, // Secondary button action
open val navigation: ErrorNavigation? = null, // Optional navigation directive
open val error: Throwable? = null, // Underlying cause
)Flexible message representation:
sealed interface UiMessage {
data class Plain(val text: String)
data class ResourceId(val resId: String)
data class Formatted(val template: String, val args: List<Any>)
}Severity levels for errors:
Info - Informational messagesWarning - Non-critical issuesError - Standard errorsCritical - Severe errors requiring immediate attentionHow the error should be displayed:
Snackbar - Brief, dismissible notificationDialog - Modal dialogFullScreen - Full-screen error stateSilent - Logged but not shown to userPredefined actions with support for custom app-specific actions:
open class ErrorAction(
open val actionId: String,
open val label: UiMessage,
open val isDestructive: Boolean = false
)
// Predefined actions
ErrorAction.Retry
ErrorAction.Dismiss
ErrorAction.Cancel
ErrorAction.Close
ErrorAction.Ok
ErrorAction.Confirm
ErrorAction.Delete // isDestructive = true
ErrorAction.Remove // isDestructive = true
// Custom actions for app-specific use cases
ErrorAction.Custom(
actionId = "skip_video",
label = UiMessage.Plain("Skip Video")
)Platform-agnostic navigation directives with support for custom routes:
open class ErrorNavigation
// Predefined navigation types
ErrorNavigation.Back
ErrorNavigation.Home
ErrorNavigation.Login
ErrorNavigation.Signup
ErrorNavigation.Settings
ErrorNavigation.Profile
ErrorNavigation.Help
ErrorNavigation.Support
ErrorNavigation.Store
// Custom navigation for app-specific routes
ErrorNavigation.Custom(route = "app://settings/payment")throw NetworkException(
id = "network_timeout",
msg = UiMessage.Plain("Unable to connect to server"),
severity = ErrorSeverity.Error,
presentation = ErrorPresentation.Snackbar,
primaryAction = ErrorAction.Retry,
error = originalException
)throw AuthException(
id = "auth_failed",
msg = UiMessage.Plain("Your session has expired"),
severity = ErrorSeverity.Error,
presentation = ErrorPresentation.Dialog,
primaryAction = ErrorAction.Retry,
navigation = ErrorNavigation.Login,
error = authError
)The library provides two ways to create custom actions:
1. Using ErrorAction.Custom (Recommended)
throw VideoException(
id = "video_playback_error",
msg = UiMessage.Plain("Unable to play video"),
severity = ErrorSeverity.Warning,
presentation = ErrorPresentation.Dialog,
primaryAction = ErrorAction.Custom(
actionId = "skip_video",
label = UiMessage.Plain("Skip Video")
),
secondaryAction = ErrorAction.Retry
)
// Handle in UI
when (actionId) {
"skip_video" -> viewModel.skipVideo()
"retry" -> viewModel.retry()
}2. Extending ErrorAction
object CustomActions {
val ContactSupport = ErrorAction(
actionId = "contact_support",
label = UiMessage.Plain("Contact Support"),
isDestructive = false
)
}
throw MyException(
primaryAction = CustomActions.ContactSupport
)The library provides two ways to create custom navigation:
1. Using ErrorNavigation.Custom (Recommended)
throw PaymentException(
id = "payment_failed",
msg = UiMessage.Plain("Payment processing failed"),
severity = ErrorSeverity.Error,
presentation = ErrorPresentation.Dialog,
primaryAction = ErrorAction.Custom(
actionId = "update_payment",
label = UiMessage.Plain("Update Payment")
),
navigation = ErrorNavigation.Custom("app://settings/payment-methods")
)
// Handle in UI
when (navigation) {
is ErrorNavigation.Custom -> navController.navigate(navigation.route)
ErrorNavigation.Login -> navController.navigate("login")
ErrorNavigation.Back -> navController.popBackStack()
}2. Extending ErrorNavigation
object AppNavigation {
val Dashboard = ErrorNavigation()
val Profile = ErrorNavigation()
}
throw MyException(
navigation = AppNavigation.Dashboard
)
// Handle in UI
when (navigation) {
AppNavigation.Dashboard -> navController.navigate("dashboard")
AppNavigation.Profile -> navController.navigate("profile")
}Your UI layer can pattern match on exceptions to present them appropriately:
try {
// Your code
} catch (e: ActionableException) {
when (e.presentation) {
ErrorPresentation.Dialog -> showDialog(
message = e.msg.toDisplayString(),
primaryButton = e.primaryAction,
secondaryButton = e.secondaryAction
)
ErrorPresentation.Snackbar -> showSnackbar(
message = e.msg.toDisplayString(),
action = e.primaryAction
)
ErrorPresentation.FullScreen -> showErrorScreen(e)
ErrorPresentation.Silent -> logger.error(e)
}
e.navigation?.let { nav ->
when (nav) {
ErrorNavigation.Login -> navigateToLogin()
ErrorNavigation.Back -> navigateBack()
ErrorNavigation.Home -> navigateToHome()
// etc.
}
}
}The library includes common exception types:
NetworkException - General network errorsAuthException - Authentication failuresClientException - Client-side errors (4xx)InternalException - Server errors (5xx)UnexpectedStatusException - Unexpected HTTP statusEmptyResponseException - Empty response bodyJsonDecodingException - JSON parsing errorsContentDecodingException - Content decoding errorsUnsupportedMediaTypeException - Unsupported content typesStorageException - Storage/disk errorsUnknownException - Unknown errorsSessionFailedException - Session validation failuresEmptyListException - Empty list/data statesEach module has comprehensive documentation with detailed examples:
The error-catalog module includes built-in internationalization support with translations for multiple languages.
import io.blackarrows.errors.catalog.i18n.DefaultMessageResolver
import io.blackarrows.errors.catalog.factories.*
// 1. Set locale once during app initialization
DefaultMessageResolver.setLocale("es") // Spanish
// 2. Use exception factory functions
throw networkException(
error = IOException("Connection failed")
)
// Error message will automatically be in Spanish:
// "La red no está disponible. Por favor, verifica tu conexión."The catalog provides convenient factory functions that automatically use the configured locale:
// Generic factories (use default error messages)
networkException() // Network error
authException() // Auth error
storageException() // Storage error
sessionFailedException() // Session error
// Specific factories (use specific error messages)
networkUnavailableException() // "Network is unavailable..."
networkTimeoutException() // "Request timed out..."
unauthorizedException() // "Your session has expired..."
tokenExpiredException() // "Your session has expired..."
forbiddenException() // "You don't have permission..."
storageUnavailableException() // "Storage is unavailable..."
insufficientSpaceException() // "Insufficient storage space..."
invalidSessionException() // "Session data is invalid..."All factory function parameters can be overridden:
// Override message (use custom text instead of i18n)
throw networkException(
msg = UiMessage.Plain("Custom network error"),
error = IOException()
)
// Override actions
throw authException(
primaryAction = ErrorAction.Dismiss,
secondaryAction = CustomActions.ContactSupport,
error = authError
)
// Override navigation
throw authException(
navigation = CustomNavigation.Onboarding,
error = authError
)
// Override presentation
throw networkException(
presentation = ErrorPresentation.Dialog, // Show as dialog instead of snackbar
error = IOException()
)
// Override everything
throw networkException(
id = "custom_network_error",
msg = UiMessage.Plain("Completely custom"),
severity = ErrorSeverity.Warning,
presentation = ErrorPresentation.Silent,
primaryAction = CustomActions.ContactSupport,
error = error
)Integrate with your own i18n system:
// Android Resources example
class AndroidResourceResolver(private val context: Context) : MessageResolver {
override fun resolve(key: String): String {
val resId = context.resources.getIdentifier(key, "string", context.packageName)
return if (resId != 0) {
context.getString(resId)
} else {
DefaultMessageResolver.resolve(key) // Fallback
}
}
override fun resolve(key: String, args: List<Any>): String {
val resId = context.resources.getIdentifier(key, "string", context.packageName)
return if (resId != 0) {
context.getString(resId, *args.toTypedArray())
} else {
DefaultMessageResolver.resolve(key, args)
}
}
}
// Use in UI layer to resolve messages
when (val msg = exception.msg) {
is UiMessage.Plain -> msg.text
is UiMessage.ResourceId -> myResolver.resolve(msg.resId)
is UiMessage.Formatted -> myResolver.resolve(msg.template, msg.args)
}See the error-catalog README for details on adding support for additional languages.
Register error reporters for logging and analytics:
// Define a custom reporter
class FirebaseErrorReporter : ErrorReporter {
override fun report(error: ActionableException) {
Firebase.crashlytics.recordException(error)
Analytics.logEvent("error_shown", mapOf(
"error_id" to error.id,
"severity" to error.severity.name
))
}
}
// Register at app startup
ErrorReporting.addReporter(FirebaseErrorReporter())
ErrorReporting.addReporter(CustomAnalyticsReporter())All modules are fully multiplatform and tested across:
arrow-errors/
├── error-core/ # Rich exception types with UI metadata
│ ├── base/ # Core types: ActionableException, ErrorAction, etc.
│ │ ├── CommonActionIds.kt # Type-safe action ID constants
│ │ └── CommonRoutes.kt # Route constants + suggestedRoute()
│ ├── extensions/
│ │ └── ThrowableExtensions.kt # toActionableException()
│ ├── network/ # Network-related exceptions
│ ├── session/ # Session-related exceptions
│ ├── mappers/ # Error mapping utilities
│ └── README.md
│
├── error-catalog/ # Centralized error messages with i18n
│ ├── ErrorCatalog.kt # Base interface
│ ├── NetworkErrorCatalog.kt # Network errors (10-XXX)
│ ├── StorageErrorCatalog.kt # Storage errors (11-XXX)
│ ├── AuthErrorCatalog.kt # Auth errors (12-XXX)
│ ├── SessionErrorCatalog.kt # Session errors (20-XXX)
│ ├── ErrorProvider.kt # Error lookup interface
│ ├── DefaultErrorProvider.kt # Default implementation
│ ├── i18n/
│ │ ├── ErrorKeys.kt # Message key constants
│ │ ├── MessageResolver.kt # Message resolution interface
│ │ ├── DefaultMessageResolver.kt # Built-in resolver
│ │ └── translations/
│ │ ├── EnglishTranslations.kt # English (default)
│ │ ├── SpanishTranslations.kt # Spanish
│ │ └── FrenchTranslations.kt # French
│ ├── factories/
│ │ └── ExceptionFactories.kt # Convenience factory functions
│ └── README.md
│
└── error-compose/ # Compose Multiplatform UI components
├── components/
│ ├── ErrorPresenter.kt # Smart routing with snackbar hoisting
│ ├── ErrorDialog.kt # Material 3 error dialog
│ ├── ErrorSnackbar.kt # Material 3 snackbar
│ └── ErrorFullScreen.kt # Full-screen error state
├── theme/
│ ├── ErrorTheme.kt # Main theme + ErrorThemeProvider
│ ├── ErrorColors.kt # Color configuration
│ ├── ErrorSpacing.kt # Spacing configuration
│ ├── ErrorTypography.kt # Typography configuration
│ └── LocalErrorTheme.kt # CompositionLocal provider
├── utils/
│ ├── MessageResolver.kt # Message resolution helpers
│ └── SeverityExtensions.kt # Severity-based styling
└── README.md
Contributions are welcome! Whether you're fixing bugs, improving documentation, adding new features, or translating error messages to new languages, your help is appreciated.
git checkout -b feature/your-feature-name
git commit -m "Description of changes"
git push origin feature/your-feature-name
# Clone the repository
git clone https://github.com/E5c11/arrow-errors.git
cd arrow-errors
# Build all modules
./gradlew build
# Run tests
./gradlew testFor questions or discussions, please open an issue on GitHub.
This project is licensed under the Apache License 2.0 - see the LICENSE file for details.
Copyright 2025 Emmanuel Conradie
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
If you find this library helpful, please consider:
Built with Kotlin Multiplatform and inspired by the need for better cross-platform error handling.
Maintained by Emmanuel Conradie
A comprehensive Kotlin Multiplatform error-handling library that provides a scalable, type-safe approach to managing exceptions with rich UI presentation metadata and centralized error messaging.
Author & Maintainer: Emmanuel Conradie
Arrow Errors helps you build consistent, user-friendly error experiences across your Kotlin Multiplatform applications. Born from the need to standardize error handling across platforms, this library transforms the way you think about exceptions—from simple error messages to rich, actionable user experiences.
Instead of throwing generic exceptions with plain string messages, Arrow Errors enables you to throw rich, actionable exceptions that carry all the information needed to present meaningful error dialogs to users. With built-in internationalization support, centralized error messaging, and ready-to-use UI components, you can create professional error experiences with minimal boilerplate.
The library consists of three complementary modules that work independently or together:
Whether you're building an Android app, iOS application, or desktop software, Arrow Errors provides a unified approach to error handling that scales with your project.
Arrow Errors is published to Maven Central. Add the dependencies you need to your Kotlin Multiplatform project.
Latest Version: Check the releases page for the current version.
For the complete error-handling solution with UI components:
// In your build.gradle.kts
// Replace $version with the latest version from releases
dependencies {
implementation("io.github.blackarrows-apps:arrow-errors-core:$version")
implementation("io.github.blackarrows-apps:arrow-errors-catalog:$version")
implementation("io.github.blackarrows-apps:arrow-errors-compose:$version")
}Choose only what you need:
error-core - Core error handling infrastructure:
dependencies {
implementation("io.github.blackarrows-apps:arrow-errors-core:$version")
}error-catalog - Centralized error messages with i18n:
dependencies {
implementation("io.github.blackarrows-apps:arrow-errors-catalog:$version")
}error-compose - Compose Multiplatform UI components:
dependencies {
implementation("io.github.blackarrows-apps:arrow-errors-compose:$version")
}Want to see Arrow Errors in action? Install the interactive Android playground:
./gradlew :sample:installDebugThe sample app demonstrates all error types, presentations, and i18n features with a comprehensive UI playground. See the sample README for more details.
Get up and running in minutes with these examples:
// Throw a rich, actionable exception
throw NetworkException(
id = "network_error",
msg = UiMessage.Plain("Unable to connect. Please check your internet connection."),
severity = ErrorSeverity.Error,
presentation = ErrorPresentation.Snackbar,
primaryAction = ErrorAction.Retry
)
// Handle in UI
try {
fetchData()
} catch (e: ActionableException) {
when (e.presentation) {
ErrorPresentation.Snackbar -> showSnackbar(e.msg)
ErrorPresentation.Dialog -> showDialog(e)
ErrorPresentation.FullScreen -> showErrorScreen(e)
ErrorPresentation.Silent -> logError(e)
}
}import io.blackarrows.errors.catalog.i18n.DefaultMessageResolver
import io.blackarrows.errors.catalog.factories.*
// Set locale once during app initialization
DefaultMessageResolver.setLocale("es") // Spanish
// Use exception factory functions (messages automatically in Spanish)
throw networkException(
error = IOException("Connection failed")
)
// Message: "La red no está disponible. Por favor, verifica tu conexión."@Composable
fun MyScreen(viewModel: MyViewModel) {
val error by viewModel.error.collectAsState()
Scaffold { padding ->
MyScreenContent(modifier = Modifier.padding(padding))
// ErrorPresenter automatically displays the right UI
ErrorPresenter(
error = error,
onDismiss = { viewModel.clearError() },
onActionClick = { actionId ->
when (actionId) {
"retry" -> viewModel.retry()
"dismiss" -> viewModel.clearError()
}
},
onNavigate = { navigation ->
when (navigation) {
ErrorNavigation.Login -> navController.navigate("login")
ErrorNavigation.Back -> navController.popBackStack()
else -> {}
}
}
)
}
}The base exception class that all errors extend. It includes:
open class ActionableException(
open val id: String? = null, // Unique ID for deduplication/logging
open val msg: UiMessage = ..., // User-facing message
open val severity: ErrorSeverity = ..., // Info, Warning, Error, Critical
open val presentation: ErrorPresentation = ..., // Snackbar, Dialog, FullScreen, Silent
open val primaryAction: ErrorAction? = null, // Primary button action
open val secondaryAction: ErrorAction? = null, // Secondary button action
open val navigation: ErrorNavigation? = null, // Optional navigation directive
open val error: Throwable? = null, // Underlying cause
)Flexible message representation:
sealed interface UiMessage {
data class Plain(val text: String)
data class ResourceId(val resId: String)
data class Formatted(val template: String, val args: List<Any>)
}Severity levels for errors:
Info - Informational messagesWarning - Non-critical issuesError - Standard errorsCritical - Severe errors requiring immediate attentionHow the error should be displayed:
Snackbar - Brief, dismissible notificationDialog - Modal dialogFullScreen - Full-screen error stateSilent - Logged but not shown to userPredefined actions with support for custom app-specific actions:
open class ErrorAction(
open val actionId: String,
open val label: UiMessage,
open val isDestructive: Boolean = false
)
// Predefined actions
ErrorAction.Retry
ErrorAction.Dismiss
ErrorAction.Cancel
ErrorAction.Close
ErrorAction.Ok
ErrorAction.Confirm
ErrorAction.Delete // isDestructive = true
ErrorAction.Remove // isDestructive = true
// Custom actions for app-specific use cases
ErrorAction.Custom(
actionId = "skip_video",
label = UiMessage.Plain("Skip Video")
)Platform-agnostic navigation directives with support for custom routes:
open class ErrorNavigation
// Predefined navigation types
ErrorNavigation.Back
ErrorNavigation.Home
ErrorNavigation.Login
ErrorNavigation.Signup
ErrorNavigation.Settings
ErrorNavigation.Profile
ErrorNavigation.Help
ErrorNavigation.Support
ErrorNavigation.Store
// Custom navigation for app-specific routes
ErrorNavigation.Custom(route = "app://settings/payment")throw NetworkException(
id = "network_timeout",
msg = UiMessage.Plain("Unable to connect to server"),
severity = ErrorSeverity.Error,
presentation = ErrorPresentation.Snackbar,
primaryAction = ErrorAction.Retry,
error = originalException
)throw AuthException(
id = "auth_failed",
msg = UiMessage.Plain("Your session has expired"),
severity = ErrorSeverity.Error,
presentation = ErrorPresentation.Dialog,
primaryAction = ErrorAction.Retry,
navigation = ErrorNavigation.Login,
error = authError
)The library provides two ways to create custom actions:
1. Using ErrorAction.Custom (Recommended)
throw VideoException(
id = "video_playback_error",
msg = UiMessage.Plain("Unable to play video"),
severity = ErrorSeverity.Warning,
presentation = ErrorPresentation.Dialog,
primaryAction = ErrorAction.Custom(
actionId = "skip_video",
label = UiMessage.Plain("Skip Video")
),
secondaryAction = ErrorAction.Retry
)
// Handle in UI
when (actionId) {
"skip_video" -> viewModel.skipVideo()
"retry" -> viewModel.retry()
}2. Extending ErrorAction
object CustomActions {
val ContactSupport = ErrorAction(
actionId = "contact_support",
label = UiMessage.Plain("Contact Support"),
isDestructive = false
)
}
throw MyException(
primaryAction = CustomActions.ContactSupport
)The library provides two ways to create custom navigation:
1. Using ErrorNavigation.Custom (Recommended)
throw PaymentException(
id = "payment_failed",
msg = UiMessage.Plain("Payment processing failed"),
severity = ErrorSeverity.Error,
presentation = ErrorPresentation.Dialog,
primaryAction = ErrorAction.Custom(
actionId = "update_payment",
label = UiMessage.Plain("Update Payment")
),
navigation = ErrorNavigation.Custom("app://settings/payment-methods")
)
// Handle in UI
when (navigation) {
is ErrorNavigation.Custom -> navController.navigate(navigation.route)
ErrorNavigation.Login -> navController.navigate("login")
ErrorNavigation.Back -> navController.popBackStack()
}2. Extending ErrorNavigation
object AppNavigation {
val Dashboard = ErrorNavigation()
val Profile = ErrorNavigation()
}
throw MyException(
navigation = AppNavigation.Dashboard
)
// Handle in UI
when (navigation) {
AppNavigation.Dashboard -> navController.navigate("dashboard")
AppNavigation.Profile -> navController.navigate("profile")
}Your UI layer can pattern match on exceptions to present them appropriately:
try {
// Your code
} catch (e: ActionableException) {
when (e.presentation) {
ErrorPresentation.Dialog -> showDialog(
message = e.msg.toDisplayString(),
primaryButton = e.primaryAction,
secondaryButton = e.secondaryAction
)
ErrorPresentation.Snackbar -> showSnackbar(
message = e.msg.toDisplayString(),
action = e.primaryAction
)
ErrorPresentation.FullScreen -> showErrorScreen(e)
ErrorPresentation.Silent -> logger.error(e)
}
e.navigation?.let { nav ->
when (nav) {
ErrorNavigation.Login -> navigateToLogin()
ErrorNavigation.Back -> navigateBack()
ErrorNavigation.Home -> navigateToHome()
// etc.
}
}
}The library includes common exception types:
NetworkException - General network errorsAuthException - Authentication failuresClientException - Client-side errors (4xx)InternalException - Server errors (5xx)UnexpectedStatusException - Unexpected HTTP statusEmptyResponseException - Empty response bodyJsonDecodingException - JSON parsing errorsContentDecodingException - Content decoding errorsUnsupportedMediaTypeException - Unsupported content typesStorageException - Storage/disk errorsUnknownException - Unknown errorsSessionFailedException - Session validation failuresEmptyListException - Empty list/data statesEach module has comprehensive documentation with detailed examples:
The error-catalog module includes built-in internationalization support with translations for multiple languages.
import io.blackarrows.errors.catalog.i18n.DefaultMessageResolver
import io.blackarrows.errors.catalog.factories.*
// 1. Set locale once during app initialization
DefaultMessageResolver.setLocale("es") // Spanish
// 2. Use exception factory functions
throw networkException(
error = IOException("Connection failed")
)
// Error message will automatically be in Spanish:
// "La red no está disponible. Por favor, verifica tu conexión."The catalog provides convenient factory functions that automatically use the configured locale:
// Generic factories (use default error messages)
networkException() // Network error
authException() // Auth error
storageException() // Storage error
sessionFailedException() // Session error
// Specific factories (use specific error messages)
networkUnavailableException() // "Network is unavailable..."
networkTimeoutException() // "Request timed out..."
unauthorizedException() // "Your session has expired..."
tokenExpiredException() // "Your session has expired..."
forbiddenException() // "You don't have permission..."
storageUnavailableException() // "Storage is unavailable..."
insufficientSpaceException() // "Insufficient storage space..."
invalidSessionException() // "Session data is invalid..."All factory function parameters can be overridden:
// Override message (use custom text instead of i18n)
throw networkException(
msg = UiMessage.Plain("Custom network error"),
error = IOException()
)
// Override actions
throw authException(
primaryAction = ErrorAction.Dismiss,
secondaryAction = CustomActions.ContactSupport,
error = authError
)
// Override navigation
throw authException(
navigation = CustomNavigation.Onboarding,
error = authError
)
// Override presentation
throw networkException(
presentation = ErrorPresentation.Dialog, // Show as dialog instead of snackbar
error = IOException()
)
// Override everything
throw networkException(
id = "custom_network_error",
msg = UiMessage.Plain("Completely custom"),
severity = ErrorSeverity.Warning,
presentation = ErrorPresentation.Silent,
primaryAction = CustomActions.ContactSupport,
error = error
)Integrate with your own i18n system:
// Android Resources example
class AndroidResourceResolver(private val context: Context) : MessageResolver {
override fun resolve(key: String): String {
val resId = context.resources.getIdentifier(key, "string", context.packageName)
return if (resId != 0) {
context.getString(resId)
} else {
DefaultMessageResolver.resolve(key) // Fallback
}
}
override fun resolve(key: String, args: List<Any>): String {
val resId = context.resources.getIdentifier(key, "string", context.packageName)
return if (resId != 0) {
context.getString(resId, *args.toTypedArray())
} else {
DefaultMessageResolver.resolve(key, args)
}
}
}
// Use in UI layer to resolve messages
when (val msg = exception.msg) {
is UiMessage.Plain -> msg.text
is UiMessage.ResourceId -> myResolver.resolve(msg.resId)
is UiMessage.Formatted -> myResolver.resolve(msg.template, msg.args)
}See the error-catalog README for details on adding support for additional languages.
Register error reporters for logging and analytics:
// Define a custom reporter
class FirebaseErrorReporter : ErrorReporter {
override fun report(error: ActionableException) {
Firebase.crashlytics.recordException(error)
Analytics.logEvent("error_shown", mapOf(
"error_id" to error.id,
"severity" to error.severity.name
))
}
}
// Register at app startup
ErrorReporting.addReporter(FirebaseErrorReporter())
ErrorReporting.addReporter(CustomAnalyticsReporter())All modules are fully multiplatform and tested across:
arrow-errors/
├── error-core/ # Rich exception types with UI metadata
│ ├── base/ # Core types: ActionableException, ErrorAction, etc.
│ │ ├── CommonActionIds.kt # Type-safe action ID constants
│ │ └── CommonRoutes.kt # Route constants + suggestedRoute()
│ ├── extensions/
│ │ └── ThrowableExtensions.kt # toActionableException()
│ ├── network/ # Network-related exceptions
│ ├── session/ # Session-related exceptions
│ ├── mappers/ # Error mapping utilities
│ └── README.md
│
├── error-catalog/ # Centralized error messages with i18n
│ ├── ErrorCatalog.kt # Base interface
│ ├── NetworkErrorCatalog.kt # Network errors (10-XXX)
│ ├── StorageErrorCatalog.kt # Storage errors (11-XXX)
│ ├── AuthErrorCatalog.kt # Auth errors (12-XXX)
│ ├── SessionErrorCatalog.kt # Session errors (20-XXX)
│ ├── ErrorProvider.kt # Error lookup interface
│ ├── DefaultErrorProvider.kt # Default implementation
│ ├── i18n/
│ │ ├── ErrorKeys.kt # Message key constants
│ │ ├── MessageResolver.kt # Message resolution interface
│ │ ├── DefaultMessageResolver.kt # Built-in resolver
│ │ └── translations/
│ │ ├── EnglishTranslations.kt # English (default)
│ │ ├── SpanishTranslations.kt # Spanish
│ │ └── FrenchTranslations.kt # French
│ ├── factories/
│ │ └── ExceptionFactories.kt # Convenience factory functions
│ └── README.md
│
└── error-compose/ # Compose Multiplatform UI components
├── components/
│ ├── ErrorPresenter.kt # Smart routing with snackbar hoisting
│ ├── ErrorDialog.kt # Material 3 error dialog
│ ├── ErrorSnackbar.kt # Material 3 snackbar
│ └── ErrorFullScreen.kt # Full-screen error state
├── theme/
│ ├── ErrorTheme.kt # Main theme + ErrorThemeProvider
│ ├── ErrorColors.kt # Color configuration
│ ├── ErrorSpacing.kt # Spacing configuration
│ ├── ErrorTypography.kt # Typography configuration
│ └── LocalErrorTheme.kt # CompositionLocal provider
├── utils/
│ ├── MessageResolver.kt # Message resolution helpers
│ └── SeverityExtensions.kt # Severity-based styling
└── README.md
Contributions are welcome! Whether you're fixing bugs, improving documentation, adding new features, or translating error messages to new languages, your help is appreciated.
git checkout -b feature/your-feature-name
git commit -m "Description of changes"
git push origin feature/your-feature-name
# Clone the repository
git clone https://github.com/E5c11/arrow-errors.git
cd arrow-errors
# Build all modules
./gradlew build
# Run tests
./gradlew testFor questions or discussions, please open an issue on GitHub.
This project is licensed under the Apache License 2.0 - see the LICENSE file for details.
Copyright 2025 Emmanuel Conradie
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
If you find this library helpful, please consider:
Built with Kotlin Multiplatform and inspired by the need for better cross-platform error handling.
Maintained by Emmanuel Conradie