# Decompose Error Propagation: How ErrorHandlers Work in Arkivanov's Library

> Learn how Decompose centralizes error propagation using thread-safe ErrorHandlers. Route library errors to custom systems with DecomposeSettings onDecomposeError callback.

- Repository: [Arkadii Ivanov/decompose](https://github.com/arkivanov/decompose)
- Tags: deep-dive
- Published: 2026-02-25

---

**Decompose centralizes non-fatal error handling through a global thread-safe `onDecomposeError` callback in `DecomposeSettings`, allowing applications to route library errors to custom logging or crash reporting systems.**

The `arkivanov/decompose` library provides a centralized mechanism for handling non-fatal errors that occur within its internal operations. Understanding Decompose error propagation is essential for production applications that need to monitor background thread violations or other recoverable failures. This article examines the architecture of the `ErrorHandlers` system, demonstrates how to install custom handlers, and traces the exact path errors take through the library's source code.

## The Global Error Handler Architecture

Decompose exposes a single configuration point for error handling through the `DecomposeSettings` class located in [`decompose/src/commonMain/kotlin/com/arkivanov/decompose/DecomposeSettings.kt`](https://github.com/arkivanov/decompose/blob/main/decompose/src/commonMain/kotlin/com/arkivanov/decompose/DecomposeSettings.kt). This design ensures that all non-fatal errors—such as main-thread check violations—flow through a consistent pipeline where they can be logged, reported, or suppressed according to application requirements.

### DecomposeSettings Configuration

The settings object stores the error handler as a lambda property `onDecomposeError: (Exception) -> Unit` with a default implementation of `printError`. According to the source at lines 14-20 of [`DecomposeSettings.kt`](https://github.com/arkivanov/decompose/blob/main/DecomposeSettings.kt), this property is part of the immutable data class that represents the library's global configuration state.

### Thread-Safe Updates with Volatile and Lock

To ensure that custom error handlers are visible across all threads without race conditions, Decompose implements an atomic update mechanism. The settings are stored in a `@Volatile` field and modified through the `DecomposeSettings.update { ... }` function, which acquires a private `Lock` before applying the supplied transformation (lines 22-36 of [`DecomposeSettings.kt`](https://github.com/arkivanov/decompose/blob/main/DecomposeSettings.kt)). This guarantees that once you install a custom handler, all subsequent errors throughout the library will be routed to your callback.

## Implementing Custom ErrorHandlers in Decompose

Applications can replace the default `printError` behavior with custom logic such as forwarding exceptions to Firebase Crashlytics, Sentry, or internal logging systems. The modern API uses the `DecomposeSettings.update` method, while a deprecated global property remains available for backwards compatibility.

### Modern API: DecomposeSettings.update

The recommended approach installs a handler atomically using the settings update block:

```kotlin
import com.arkivanov.decompose.DecomposeSettings

// Install a handler that forwards exceptions to a crash-reporting service.
DecomposeSettings.update { current ->
    current.copy(onDecomposeError = { exception ->
        CrashAnalytics.log(exception)          // Your own logging / reporting
        // Optionally rethrow if you want the app to crash
        // throw exception
    })
}

```

This pattern ensures thread safety and aligns with the implementation in [`DecomposeSettings.kt`](https://github.com/arkivanov/decompose/blob/main/DecomposeSettings.kt) (lines 31-36).

### Deprecated Shortcut: Global onDecomposeError

For legacy code, Decompose exposes a top-level property in [`ErrorHandlers.kt`](https://github.com/arkivanov/decompose/blob/main/ErrorHandlers.kt) that forwards to the settings object:

```kotlin
import com.arkivanov.decompose.errorhandler.onDecomposeError

onDecomposeError = { ex ->
    Log.e("Decompose", "Caught error", ex)
}

```

As noted in the source at lines 8-12 of [`decompose/src/commonMain/kotlin/com/arkivanov/decompose/errorhandler/ErrorHandlers.kt`](https://github.com/arkivanov/decompose/blob/main/decompose/src/commonMain/kotlin/com/arkivanov/decompose/errorhandler/ErrorHandlers.kt), this property is marked `@Deprecated` and delegates to `DecomposeSettings.settings.onDecomposeError`.

## How Decompose Propagates Errors Internally

When the library detects a recoverable error, it invokes the `onDecomposeError` lambda directly from the current `DecomposeSettings`. A concrete example occurs in the main-thread checker, which validates that certain operations occur on the UI thread.

### Main-Thread Check Example

In [`decompose/src/jvmMain/kotlin/com/arkivanov/decompose/mainthread/CheckMainThread.kt`](https://github.com/arkivanov/decompose/blob/main/decompose/src/jvmMain/kotlin/com/arkivanov/decompose/mainthread/CheckMainThread.kt) (lines 4-9), the library detects a background thread access when `mainThreadCheckEnabled` is true:

```kotlin
// Inside Decompose's main-thread checker (JVM)
if (DecomposeSettings.settings.mainThreadCheckEnabled && mainThreadChecker?.isMainThread() == false) {
    onDecomposeError(
        NotOnMainThreadException(currentThreadName = Thread.currentThread().name)
    )
}

```

Because `onDecomposeError` references the function stored in the global settings, any custom handler installed by the application receives the `NotOnMainThreadException` immediately.

## Default Error Handling Behavior

If no custom handler is installed, Decompose defaults to `printError`, an `expect/actual` function defined in [`decompose/src/commonMain/kotlin/com/arkivanov/decompose/errorhandler/PrintError.kt`](https://github.com/arkivanov/decompose/blob/main/decompose/src/commonMain/kotlin/com/arkivanov/decompose/errorhandler/PrintError.kt). The JVM implementation (lines 3-5 of [`decompose/src/jvmMain/kotlin/com/arkivanov/decompose/errorhandler/PrintError.kt`](https://github.com/arkivanov/decompose/blob/main/decompose/src/jvmMain/kotlin/com/arkivanov/decompose/errorhandler/PrintError.kt)) simply delegates to `exception.printStackTrace()`, ensuring that errors are visible during development while remaining non-fatal.

## Summary

- **Global Configuration**: Decompose error propagation relies on `DecomposeSettings`, a thread-safe singleton that stores the `onDecomposeError` callback.
- **Atomic Updates**: Use `DecomposeSettings.update { ... }` to install custom handlers safely across threads without race conditions.
- **Internal Propagation**: The library invokes `onDecomposeError` directly when detecting recoverable issues like main-thread violations, routing exceptions to your custom logic or the default `printError` implementation.
- **Legacy Support**: A deprecated global `onDecomposeError` property remains available for backwards compatibility but delegates to the settings object.

## Frequently Asked Questions

### How do I set a custom error handler in Decompose?

Update the global settings using `DecomposeSettings.update { it.copy(onDecomposeError = { exception -> /* your logic */ }) }`. This approach is thread-safe and ensures your handler is visible to all components immediately.

### What happens if I don't configure a custom ErrorHandler?

If no custom handler is installed, Decompose defaults to `printError`, which prints the exception stack trace to the console (via `printStackTrace()` on JVM). This behavior is non-fatal and serves as a development aid.

### Is the deprecated `onDecomposeError` global property safe to use?

While the deprecated global property still functions by forwarding to `DecomposeSettings.settings.onDecomposeError`, you should migrate to `DecomposeSettings.update` for thread safety and to avoid deprecation warnings. The legacy property exists solely for backwards compatibility.

### When does Decompose actually trigger the error handler?

Decompose invokes the handler for non-fatal recoverable errors, most notably when `mainThreadCheckEnabled` is true and a component is accessed from a background thread. In this case, it creates a `NotOnMainThreadException` and passes it to `onDecomposeError`.