Skip to content

Error Types

Declare named failure variants and use explicit or inferred fallible function contracts.

Updated View as Markdown

An error declaration creates a named closed error type.

error LoadError {
    NotFound { id: int }
    PermissionDenied
    Storage { message: string }
}

Error and variant names use PascalCase.

Variants

Like enum variants, error variants can be unit, tuple-style, or struct-style.

error ParseError {
    Empty
    InvalidCharacter(char)
    InvalidDigit { position: int, found: char }
}

Prefer named fields when a payload has several values or its meaning would not be obvious from position.

Error types participate in pattern matching and exhaustiveness checking.

match error {
    Empty => "empty input"
    InvalidCharacter(found) => "invalid $found"
    InvalidDigit { position, found } => "$found at $position"
}

Signatures

T ! E declares a function that succeeds with T or fails with E.

fn load_user(id: int): User ! LoadError {
    // ...
}

It is source sugar for Result[T, E]. Internally, callers receive an ordinary result value.

Use an explicit error type on public APIs, host boundaries, trait requirements, and functions whose failure contract should not change when their body changes.

Error exits

Inside a fallible function, error value exits through the error channel.

fn validate_id(id: int): int ! LoadError {
    if id < 1 {
        error NotFound { id }
    }
    return id
}

The expected error type resolves an unambiguous bare variant. If several reachable error types define the same variant name, construct an explicitly qualified value in the Result form.

return Err(value) is the explicit result form.

return Err(LoadError.NotFound { id })

The current language does not automatically log one form and suppress another. Logging policy belongs to application or host code.

Success exits

A compatible bare return is lifted to Ok in a fallible function.

return user

return Ok(user) is also valid. Choose the style that best exposes the local control flow.

Inference

An unannotated internal function can infer a fallible result from error statements and result-propagating ? sites.

fn internal(id: int) {
    if id < 1 {
        error NotFound { id }
    }
    return load_user(id)?
}

The compiler infers compatible success types and an error type or anonymous error union.

For example, propagating two independent error types creates one synthetic union for the inferred function:

error CacheError { Unavailable }
error StoreError { Disconnected }

fn read_cache(): User ! CacheError {
    error Unavailable
}

fn read_store(): User ! StoreError {
    error Disconnected
}

fn choose(cached: bool) {
    if cached {
        return read_cache()?
    }
    return read_store()?
}

Here choose has a User success type and an inferred error side containing both CacheError and StoreError. The union is closed over the error types observed during checking; it does not behave like a dynamically extensible exception set.

Inference is useful during implementation, but a named error declaration plus explicit signature is easier to evolve and document at an API boundary.

An explicit non-fallible return annotation is a constraint, not a hint. If choose were declared fn choose(cached: bool): User, either ? would conflict with that signature. Declare User ! ErrorType, remove the propagation, or leave an internal function unannotated for inference.

The projection choose::err names the inferred error side without repeating its members. See Projections.

Display

Do not assume every error declaration receives a generated human message or automatic structured logging. Implement or derive Display where a textual representation is required, and preserve structured variants for program decisions.

impl Display for LoadError {
    fn to_string(self): string {
        return match self {
            NotFound { id } => "user $id was not found"
            PermissionDenied => "permission denied"
            Storage { message } => message
        }
    }
}

Design

Keep variants actionable and stable:

  • encode a category as the variant;
  • put decision-relevant data in typed fields;
  • avoid requiring callers to parse a message;
  • separate transport/backend detail from portable domain failures;
  • use an error union when a boundary genuinely exposes several existing error domains.
Navigation

Type to search…

↑↓ navigate↵ selectEsc close