Error Propagation: What It Is, Error Propagation Mechanism, and How It Works in Mobile Development

Author: IT Sectr Published: 2026-05-26 Reading time: 9 min

Error Propagation is a mechanism for propagating an error up the call stack from the point of occurrence to the handler. When a function cannot handle an error on its own, it passes it to the calling party through an exception, throws declaration, or Return Type. Proper implementation of propagation is critical for the stability of mobile applications: unhandled or incorrectly passed errors lead to crashes. According to Apple Swift Documentation (2026), automatic propagation via throws in Swift allows passing an error to any level without boilerplate code.

Key Takeaways

  • Error Propagation is passing an error from where it occurred up the stack to a handler, bypassing intermediate functions
  • Automatic propagation via throws in Swift passes the error without explicit code at each stack level
  • Manual propagation in Kotlin and Dart requires explicit try-catch or passing in a Result container at each level
  • Checked exceptions in Java force propagation via throws in the signature, unchecked exceptions can be ignored
  • Result Type is an alternative to exceptions where the error is passed as a value without stack unwinding

What Is Error Propagation?

Error Propagation is the process of passing an error object from the function where it occurred up the call chain to the nearest suitable handler. Imagine the call stack: ViewController calls ViewModel, ViewModel calls Repository, Repository calls API. If the API returns a network error, it must go through Repository and ViewModel to ViewController, which will display a message to the user. Each intermediate function decides: handle the error or pass it further (propagate).

There are two approaches to propagation: automatic and manual. With automatic approach (Swift throws, Java checked exceptions) the compiler forces the developer to either handle the error or declare propagation in the signature. With manual approach (Result Type, Kotlin Try) the error is passed as a value — the developer explicitly writes code to pass or transform the error. According to Kotlin Result Docs (2026), Result<T> in Kotlin is not intended for propagation across function boundaries directly — it needs to be transformed or handled at each level, which makes propagation more conscious but also more verbose.

The choice of approach depends on the application architecture and language. Swift is dominated by automatic propagation via throws, Kotlin uses a mix of exceptions (for unexpected errors) and Result-like containers (for expected ones). It is important to understand: propagation is not a goal, but a necessity. An ideal architecture minimizes the depth of propagation, handling errors at the lowest possible level where there is enough context to make a decision.

Propagation via Throws in Swift

In Swift, propagation via throws is automatic: if function A with throws calls function B with throws, and A does not handle B's error in do-catch, the error is automatically passed to A's caller. This eliminates the boilerplate code typical of Java checked exceptions, where throws must be declared in every method of the chain. Swift uses the principle “one throws-function in the chain = the whole chain becomes throws, if not handled at intermediate levels”.

swift
struct UserRepository {
    func fetchUser(id: Int) throws -> User {
        let data = try networkService.request(path: "/users/\(id)")
        return try parseUser(from: data)
    }
}

class UserViewModel {
    let repo = UserRepository()

    func loadUser(id: Int) throws -> User {
        return try repo.fetchUser(id: id)
    }
}

// ViewController — final handler
func onButtonTap() {
    let vm = UserViewModel()
    do {
        let user = try vm.loadUser(id: 42)
        updateUI(user)
    } catch {
        showError("Failed to load user")
    }
}

Propagation chain: networkService.request -> fetchUser -> loadUser -> onButtonTap. Each intermediate function is marked throws and does not contain do-catch — the error is automatically passed upward. ViewController onButtonTap is the final handler with do-catch. If ViewModel decided to transform the error (wrap it into another type), it could use do-catch and a new throw. Automatic propagation reduces code: Repository does not need to know how to handle the error — that is the responsibility of ViewController, which has access to the UI to display a message to the user.

Propagation via Exceptions in Kotlin

In Kotlin, propagation via exceptions does not require a throws declaration in the signature (all exceptions are unchecked). The exception automatically rises up the stack until it encounters a try-catch. However, the absence of throws in the signature makes propagation implicit: the developer cannot see from the function signature that it might throw an exception. This is both a plus (less boilerplate) and a minus (easier to forget to handle) at the same time. Kotlin solves this problem through conventions and architectural patterns, not through the language itself.

kotlin
class UserRepository(
    private val api: ApiService,
    private val db: Database
) {
    suspend fun getUser(id: String): User {
        return try {
            api.fetchUser(id)
        } catch (e: IOException) {
            db.getCachedUser(id) ?: throw AppException("User unavailable")
        }
    }
}

class UserViewModel(private val repo: UserRepository) {
    private val _state = MutableStateFlow<UiState<User>>(UiState.Loading)
    val state: StateFlow<UiState<User>> = _state

    fun loadUser(id: String) {
        viewModelScope.launch {
            try {
                val user = repo.getUser(id)
                _state.value = UiState.Success(user)
            } catch (e: AppException) {
                _state.value = UiState.Error(e.message ?: "Unknown")
            }
        }
    }
}

In Repository, propagation with transformation: on IOException (network unavailable) the function tries to get cached data from the database. If the cache is empty, it throws AppException — propagation continues with a new error type. ViewModel catches AppException and translates it to UiState.Error — the error does not go further, propagation ends at the UI layer level. Kotlin Coroutines add specifics: exceptions in launch automatically propagate through CoroutineExceptionHandler, and in async — only when await() is called. This is important to consider when designing propagation in coroutines — SupervisorJob prevents cancellation of the parent coroutine upon an error in a child coroutine.

Propagation via Result Type

An alternative to exceptions is propagation through a container type that passes success or error as a value. In this approach, the function returns not a value but a wrapper: Result<T, E> in Swift, Result<T> in Kotlin, Either<L, R> in Dart (from the fpdart or dartz package). The error does not unwind the stack — it simply lies in the container, and the next level decides what to do with it. This makes propagation more explicit and controllable.

kotlin
data class HttpResult<out T>(
    val data: T?,
    val error: AppError?
) {
    val isSuccess: Boolean get() = data != null
    val isError: Boolean get() = error != null
}

sealed class AppError {
    data class Network(val message: String) : AppError()
    data class Auth(val message: String) : AppError()
}

fun fetchUser(id: String): HttpResult<User> {
    return try {
        val response = api.get("/users/$id")
        HttpResult(data = parseUser(response), error = null)
    } catch (e: IOException) {
        HttpResult(data = null, error = AppError.Network("No internet"))
    }
}

HttpResult<T> is a simple container with data and error fields. Sealed class AppError defines error types (Network, Auth). The fetchUser function returns HttpResult, propagation does not require stack unwinding — the caller simply checks isSuccess/isError. This approach is especially useful in Clean Architecture, where each layer (data, domain, presentation) can transform the error: IOError -> DomainError -> UiError. Propagation through a container makes these transformations explicit and testable, unlike exceptions, where the transformation chain is not visible in function signatures.

Propagation vs Handling: When to Pass, When to Handle

One of the key decisions in error handling design is choosing between propagation (pass upward) and handling (handle here). The decision rule: handle the error at the level where there is enough context for a meaningful action. If you have access to the UI — show a message to the user. If you have access to the cache — try to recover. If you have neither — propagate.

ScenarioActionRationale
Network error in RepositoryPropagateRepository does not know if the user wants to retry the request
Parsing error in RepositoryHandle (return default)Repository knows the format, can return a fallback value
Timeout in ViewModelHandle (UiState.Error)ViewModel manages UiState, knows how to translate the error
Authorization error in InterceptorHandle (refresh token)Interceptor has access to tokens and can restore the session
Unknown error in UseCasePropagateUseCase has no UI context — only business logic

The golden rule: minimum propagation, maximum handling at lower levels. If Repository can recover from the cache — it should do so without passing the error upward. If ViewModel can show a Snackbar — let it show, without requiring additional code from ViewController. Each level of propagation increases coupling and complicates testing. According to Google Android Architecture Guide (2026), it is recommended to minimize propagation across layer boundaries by using sealed class UiState to represent all possible states (Loading, Success, Error) at the ViewModel level, and not to pass exceptions directly to the UI layer.

Problems and Antipatterns of Error Propagation

Incorrect propagation is a source of hard-to-find bugs in mobile applications. Let us consider five main problems that developers face and ways to solve them.

Loss of error context

The most common problem: during propagation, an exception is caught, logged, and a new one is thrown without the original exception. The developer loses the StackTrace and cannot understand where exactly the error occurred. In Swift, use error chaining: throw MyError(context: originalError). In Kotlin: throw AppException(cause = originalException). In Dart: throw AppException(message, originalException). Never create a new exception without passing cause/underlyingError.

Ignoring the error (empty catch)

catch (e: Exception) { /* nothing */ } is an antipattern that causes the application to continue working in an incorrect state. If you are sure the error can be ignored — add a comment with justification. In Swift, use try? for optional ignoring (error -> nil). In Kotlin — Result<T>.onFailure { /* log */ }. Do not swallow exceptions without logging.

Excessive propagation depth

If an error passes through 5+ levels without handling, the architecture needs review. Each propagation level is a dependency on the throws signature of underlying functions. Solution: use Failure-containers (sealed class Result { Success, Error }) at layer boundaries to make propagation explicit and limited. The shorter the propagation chain, the easier it is to test and debug the code.

Propagation in coroutines without SupervisorJob

In Kotlin Coroutines, an exception in launch by default cancels the parent coroutine and all siblings (children of the same scope). If one of 10 parallel tasks fails, the other 9 will be cancelled, which is often undesirable. Use SupervisorJob or supervisorScope to isolate errors: an error in one child does not cancel siblings. ViewModelScope uses SupervisorJob by default, which saves from this problem in Android.

Propagation through callback without handling

In callback-based APIs, the error is often passed as a callback parameter. If the callback does not handle the error (or handles it incorrectly), propagation becomes implicit and easily lost. Solution: migrate to async/await (Swift) or coroutines (Kotlin), where propagation works through standard try-catch mechanisms. If callback is unavoidable — use Either<Error, T> or Result<T> for forced handling of both cases.

Frequently Asked Questions

How is Error Propagation different from throw?

Throw is a one-time action of throwing an exception. Error Propagation is the entire process of passing an error through several stack levels, from throw to catch. Propagation includes throw, automatic or manual passing through intermediate functions, and final handling. It is a broader concept describing the error's lifecycle.

How to test Error Propagation?

Use mock objects that throw exceptions in given scenarios. Check that the function correctly propagates or handles the error via assertThrows (Kotlin/JUnit) or XCTAssertThrowsError (Swift/XCTest). For Result-based propagation, check isSuccess/isError and values in both cases.

When is propagation via Result better than exceptions?

Result propagation is preferable for expected errors (invalid data, business rules) within one architectural boundary. Exceptions are better for unexpected errors (network loss, I/O errors) that should be handled at a high level. A result with an error does not interrupt the execution flow, an exception does.

How to propagate an error through Kotlin coroutines?

In Kotlin Coroutines, an exception in launch automatically propagates through CoroutineScope with cancellation of siblings. Use supervisorScope or SupervisorJob for isolation: an error in one coroutine does not cancel others. For async, the error must be explicitly handled via try-catch when calling await(), otherwise it will be swallowed.

Which level should be the final error handler?

The ideal final handler is the UI layer (ViewController, Fragment/Composable). Only it has access to the user interface and can display a message, Snackbar, or dialog. Intermediate layers (Repository, UseCase, ViewModel) propagate the error, transforming it if necessary into a more abstract domain type.

Summary

  • Error Propagation is passing an error up the stack from where it occurred to a handler via exceptions or Result containers
  • Automatic propagation in Swift via throws requires no code at intermediate levels — the error rises on its own
  • Manual propagation in Kotlin via explicit try-catch and throw at each level makes handling conscious but verbose
  • Result Type passes the error as a value without stack unwinding, convenient for expected errors in Clean Architecture
  • Handling rule: handle at the level with context (UI), propagate through levels without context (domain, data)
  • Antipatterns: empty catch, loss of cause during transformation, excessive propagation depth, ignoring SupervisorJob
  • Design typed errors (sealed class / enum) for each layer and transform them when crossing layer boundaries

We will develop a mobile application turnkey

IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.

Discuss the project

Read also