An error union combines existing error types.
error AuthError {
InvalidToken
}
error DbError {
ConnectionFailed
}
error AppError = AuthError | DbErrorAppError 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 | NetworkErrorThe 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 | QueryErrorQualify 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.