Skip to content

Error Types

Accepted

Accepted for V1 source semantics; public error-set reflection APIs are deferred.

Errors are cheap scoped tag values, not exception objects. They do not carry payloads, stack traces, heap allocations, messages, or rich diagnostic records. APIs that need rich failure context should return explicit data, write to a diagnostic sink, or use a result/container type from std.

Error Set Declarations

Error-set declarations bind Type values:

const ReadError = error {
  FileNotFound,
  PermissionDenied,
  InvalidUtf8,
}

ReadError is a constant whose value has type Type. error { ... } creates a closed error-set type value. The canonical spelling omits a type annotation. When explanation needs an annotation, use Type:

const ReadError: Type = error { FileNotFound }

Error sets are ordinary type values. A Type value is a handle to a compiler type-table entry. A non-empty error { ... } expression creates a fresh closed error-set type entry for that semantic instance. Re-evaluating the same cached semantic instance returns the same handle; evaluating the constructor in a different semantic instance creates a different handle.

Aliases copy the type handle, not the type entry:

const ReadAgain = ReadError

ReadAgain.FileNotFound refers to the same case identity as ReadError.FileNotFound.

Top Error-Set Type

Error is the top error-set type value:

fn read(path: Path) []u8!Error

Error is provided by the prelude and conceptually defined from an open compiler intrinsic:

const Error: Type = Compiler.top_error_set()

Compiler.top_error_set() returns the canonical top error-set type value. It does not create a fresh type. Sema operates on the compiler-owned type value, not on the spelling Error.

The stable qualified spelling is prelude.Error. If an ordinary scope shadows the imported name Error, prelude.Error still refers to the top error-set type value.

Error is a concrete sized runtime type for any error value, including values from anonymous error sets. It is not a finite closed set and is not enumerable.

Compatibility is one-way:

  • any closed error set or closed error-set union can coerce to Error;
  • Error does not implicitly coerce down to a closed error set or union.

Error may also be used as a constraint for comptime error-set type parameters:

fn accepts_error_set(comptime E: Error) void {}

Here E is a comptime error-set Type value, not a runtime error value.

Named closed error sets are preferred for public APIs whose possible cases are part of the contract. Use Error for callback boundaries, plugin surfaces, test helpers, and adapters that deliberately accept or erase any error.

Closed and Empty Sets

Error sets are closed by default. A declared error set contains exactly its listed cases.

Closed error sets and closed error-set unions are concrete sized runtime types. Their values are cheap tag-only values with compiler-defined representation.

error {} is valid and returns the canonical empty error-set type value. The empty error set is the bottom of the error-set lattice:

const NoError = error {}

All empty error sets are semantically equal and coerce to any error set, any error-set union, and Error.

Not unspecified failure

Empty error sets do not mean unspecified failure; they mean no possible error values.

T!(error {}) is a distinct error-return shape from T. It is valid, but lintable as errors/unnecessary-fallible-empty-error when the fallible shape is unnecessary. The empty error set does not make a fallible value auto-unwrap in non-error-returning contexts.

Error Cases

Error cases are tag-only. They do not carry payloads.

Use diagnostics or explicit result data for rich context:

fn parse(source: Source)
  diag: *Diagnostics
  Ast!ParseError

Qualified spelling uses the containing error set:

ReadError.FileNotFound

ErrorSet.Case has the static type of ErrorSet. It does not create a singleton literal independent of its owner.

In positions with an expected closed error set, shorthand is allowed when the expected set contains a unique matching case:

return .FileNotFound

For a closed error-set union, leading-dot shorthand is valid only when exactly one contained scoped case has that name. If multiple member cases share the same textual name, use qualified spelling. The top Error type never supplies leading-dot case context; compare or return qualified cases instead.

Free unqualified case names are not valid.

Duplicate Case Names

Duplicate names inside the same error set are invalid:

const ReadError = error {
  Timeout,
  Timeout,
}

Different error sets may contain cases with the same name:

const FileError = error {
  Timeout,
}

const NetworkError = error {
  Timeout,
}

These cases are distinct scoped identities:

FileError.Timeout
NetworkError.Timeout

This mirrors enum behavior. The qualifier is part of the source identity. If both cases appear in the same union, .Timeout is ambiguous and must be qualified.

Case names may also match the owner or a prelude type such as Error; those forms are valid but lintable as errors/confusing-case-name:

const Timeout = error {
  Timeout,
}

Error Set Unions

Error sets compose with |:

const LoadError = ReadError | AllocError

A union of closed error sets is a closed error-set type value containing the scoped case identities of its operands.

Semantic equality ignores grouping and order:

(ReadError | ParseError) | AllocError == ReadError | (ParseError | AllocError)

Duplicate collapse happens only for the same scoped case identity, such as through aliases. Same textual names from different owning sets remain distinct.

Union normalization follows the error-set lattice:

ReadError | error {} == ReadError
ReadError | Error == Error

The operands must be valid for the same accepted | family. ReadError | i32 is invalid.

Inline composition is allowed, but composed error types in T!E must be parenthesized:

fn load(path: Path) Module!(ReadError | AllocError)

These forms are invalid:

fn load(path: Path) Module!ReadError | AllocError
fn load(path: Path) Module! error { InvalidModule }

Use parentheses for anonymous error sets in the error slot:

fn load(path: Path) Module!(error { InvalidModule })

Named error sets are preferred for public APIs. Public anonymous error sets and public anonymous error-set operands are lintable as errors/public-anonymous-error-set because callers lack a stable qualified owner for the cases.

Unaddressable duplicate cases

A union such as error { A } | error { A } contains two distinct cases that cannot be qualified later. This is valid source, but lintable as errors/unaddressable-duplicate-anonymous-cases. Bind the sets first when both cases need to be addressable.

Inferred Error Sets

T! infers the error set from a function body after sema:

fn load(path: Path) Module! {
  var bytes = try read(path)
  var ast = try parse(bytes)
  lower(ast)
}

The inferred error set is the closed union of existing error sets that can leave the function through its error-return path. Inference collects:

  • error sets propagated by try;
  • fallible tail expressions and returned fallible expressions;
  • explicitly returned qualified error values such as return LoadError.InvalidModule;
  • errors propagated by try inside catch blocks.

T! inference does not invent local error sets from bare cases:

fn load(path: Path) Module! {
  return .InvalidModule
}

If a function owns a domain error case, give it an explicit error set:

const LoadError = ReadError | ParseError | error {
  InvalidModule,
}

fn load(path: Path) Module!LoadError {
  return .InvalidModule
}

If inference has no contributed errors, it infers the canonical empty error set and is lintable as errors/unnecessary-fallible-empty-error when the fallible shape is unnecessary.

Public APIs with inferred error sets are allowed, but lintable as api/public-inferred-error-set because the public contract can drift with implementation changes.

Public inference that resolves to Error is lintable as api/public-top-error-inference and can suggest spelling T!Error explicitly if top-error erasure is intended.

Bare T! is inference syntax for function declarations or future function literals with bodies. Bodyless function type expressions and required contract operation signatures must spell a complete error set, such as T!ReadError or T!Error.

Recursive or mutually recursive inference cycles require an explicit error set in V1.

Error Values

An error case value has the containing error-set type:

var err: ReadError = .FileNotFound

For unions:

var err: LoadError = ReadError.FileNotFound

Compatible error values can coerce into an error-return type:

fn read(path: Path) []u8!ReadError {
  return .FileNotFound
}

Success values of type T can also coerce into T!E.

Error values support equality comparison when both operands can be viewed through a valid common error type. The top Error type is a valid common type:

fn is_oom(err: Error) bool {
  err == AllocError.OutOfMemory
}

Closed-set comparisons against impossible cases are diagnostics. The compiler does not synthesize an implicit union solely to compare unrelated closed error sets. There is no ordering by default.

Error Reflection

Closed error sets and closed error-set unions are known to sema. Compiler tooling can inspect their finite scoped cases for diagnostics, lints, documentation, and signature display. Error is the top error-set type value and is not enumerable as a closed finite set.

Anonymous error cases that flow through Error must retain stable diagnostic identity. The exact rendering format for anonymous cases belongs to diagnostics/tooling, not V1 source semantics.

Deferred reflection API

Future comptime reflection over error sets is desired, but the exact public API is deferred and tracked by Errors. This deferral does not block compiler diagnostics, linting, documentation, or signature display.