Skip to content

Error Unions

Flatten several named error types into one compatible boundary.

Updated View as Markdown

An error union combines existing error types.

error AuthError {
    InvalidToken
}

error DbError {
    ConnectionFailed
}

error AppError = AuthError | DbError

AppError can be used as the error side of a fallible function.

fn handle(): Response ! AppError {
    user := authenticate()?
    data := load_data(user)?
    return render(data)
}

Flattening

A member error propagates into the union without an extra wrapper variant.

fn authenticate(): User ! AuthError {
    error InvalidToken
}

fn handle(): User ! AppError {
    return authenticate()?
}

Nested unions flatten.

error NetworkError { Timeout }
error ServiceError = AuthError | DbError
error AppError = ServiceError | NetworkError

The final boundary contains variants from all three member error types.

Flattening preserves the identity of each constituent error declaration. It removes nested union layers; it does not merge two declarations merely because their variants have equal names and payloads.

Inferred unions

An unannotated function that propagates several error types with ? can receive a synthetic error union.

fn refresh() {
    session := authenticate()?
    return load_data(session)?
}

If authenticate fails with AuthError and load_data fails with DbError, the inferred error side contains both. Callers can propagate it, catch it, or refer to it as refresh::err.

Inference is body-sensitive: adding a new propagated call can enlarge the union. Prefer a declared union at a public boundary so the API changes only when its declaration changes.

Matching

A match over the union must cover every reachable member variant.

fn status(error: AppError): int {
    return match error {
        InvalidToken => 401
        ConnectionFailed => 503
    }
}

The checker reports missing variants across the complete flattened set.

Ambiguity

If two member errors define the same variant name, a bare pattern is ambiguous.

error BookingError { Timeout }
error QueryError { Timeout }
error ServiceError = BookingError | QueryError

Qualify the variants.

match error {
    BookingError.Timeout => retry_booking()
    QueryError.Timeout => retry_query()
}

Qualification preserves the constituent error identity even when the variant names and payloads happen to match.

Type groups

Match a whole member error type with binding: ErrorType.

match error {
    auth: AuthError => handle_auth(auth)
    database: DbError => handle_database(database)
}

A type-group arm covers every variant belonging to that constituent. This is useful for delegating to a subsystem-specific handler.

Type-group coverage is constituent-based. Once an arm covers AuthError, later arms for individual AuthError variants are unreachable.

Compatibility

Propagation is valid from a member into its union and through nested unions. The reverse is not implicit: an arbitrary AppError cannot flow into a function accepting only AuthError.

Use catch to narrow, map, or recover at that boundary.

Compatibility is transitive through nested unions. A value of AuthError can flow into ServiceError, and then into AppError when those declarations form that chain. Compatibility does not imply a general conversion between ordinary anonymous unions and error unions.

Design

Use an error union when a layer intentionally exposes failures from several existing subsystems.

Prefer a new domain error with explicit mapping when callers should not depend on backend-specific variants. Error unions preserve constituent detail, which is useful internally but can make a public API grow whenever a dependency adds a failure case.

Type projections such as function::err can track an inferred union. See Projections.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close