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 userreturn 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.