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

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. 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, 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). 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:

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 (lines 31-36).

Deprecated Shortcut: Global onDecomposeError

For legacy code, Decompose exposes a top-level property in ErrorHandlers.kt that forwards to the settings object:

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, 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 (lines 4-9), the library detects a background thread access when mainThreadCheckEnabled is true:

// 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. The JVM implementation (lines 3-5 of 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →