Skip to content

Error Handling

Accepted

Accepted for V1 try and catch source semantics.

Error handling is explicit. try propagates compatible errors, while catch handles errors locally with ordinary expression and block typing.

try

try unwraps an error-returning expression or returns the same error from the current function:

var bytes = try read(path)

try expr is conceptually equivalent to catching the error and returning it:

expr catch as err {
  return err
}

try does not remap, wrap, narrow, or widen an explicit caller error type. It only propagates when the callee error set is compatible with the enclosing function's error set. If the caller uses T!, the propagated error set contributes to the inferred error set.

try is invalid in a function that cannot return errors:

fn main() void {
  var bytes = try read(path) // invalid
}

Use catch to handle locally in non-error-returning functions.

try requires an error-returning expression with a success type. It is not valid on a bare error value:

try ReadError.FileNotFound

Error Compatibility

Propagation is compatible when the callee error set is already part of the caller error set.

const LoadError = ReadError | error {
  InvalidModule,
}

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

Explicit T!E signatures are closed contracts. try cannot automatically widen an explicit E; use T! when widening should be inferred. Top Error cannot propagate into a closed set through try.

There is no implicit name-based remapping. Same-named cases from different error sets are distinct scoped cases.

catch

catch handles the error path of an error-returning expression:

var bytes = read(path) catch {
  empty_bytes
}

catch is only for error-returning expressions, not optionals, bare error values, or general unions.

var bytes = ReadError.FileNotFound catch {
  empty_bytes
}

try expr catch { ... } is invalid. Use either try for propagation or catch for local handling.

An error-returning expression used as a statement without try or catch is invalid:

read(path)

If the success value should be discarded, handle the error path first and assign the resulting value to _:

_ = save(path, bytes) catch {}

Catch Binding

The caught error may be bound with catch as name:

var bytes = read(path) catch as err {
  if err == ReadError.FileNotFound {
    empty_bytes
  } else {
    diag.err(err)
    return LoadError.LoadFailed
  }
}

Use catch { ... } or catch fallback when the error value is not needed.

V1 catch binds at most one immutable error value name. Destructuring, mutable catch bindings, and pattern catch arms are rejected. The binding has the static error type of the caught expression; a T!Error expression binds err: Error.

Error equality tests do not narrow the binding type in V1. Inside if err == ReadError.FileNotFound, err keeps its original static error type.

Catch Grammar

catch as name must be followed by a block expression:

var bytes = read(path) catch as err {
  diag.err(err)
  empty_bytes
}

Without as, catch parses the following fallback operand as an unbound fallback:

var bytes = read(path) catch empty_bytes
var bytes2 = read(path) catch (empty_bytes)

This means catch (err) is a parenthesized fallback expression, not a binding. If that expression does not type-check as the success type, diagnostics should suggest catch as err { ... } when the likely intent was binding the error value.

These forms are invalid in V1:

read(path) catch as err
read(path) catch as err => empty_bytes
read(path) catch as err (fallback)
read(path) catch (err) { empty_bytes }

Future pattern matching may add a catch switch form that handles error cases directly. V1 keeps catch as as the only error-binding catch form and rejects catch patterns.

Catch Result Type

expr catch { ... }, expr catch as err { ... }, and expr catch fallback have the success type of expr.

The block or fallback expression must produce the success type unless it diverges:

var bytes = read(path) catch {
  empty_bytes
}

var bytes2 = read(path) catch return LoadError.LoadFailed

An empty catch block has type void, so it recovers only void!E expressions:

save(path, bytes) catch {}

This is invalid when the success type is not void:

var bytes = read(path) catch {}

A fallible fallback call does not implicitly flatten into the outer catch expression. Use try or nested catch explicitly:

fn load(path: Path) Module! {
  var bytes = read_cache(path) catch {
    try read_disk(path)
  }

  parse(bytes)
}

In this example, handled cache errors do not escape unless the catch block returns them. Errors from try read_disk(path) contribute to the enclosing function's inferred error set.

Catch Fallback Shorthand

catch fallback catches any error and produces fallback:

var bytes = read(path) catch empty_bytes

catch return SomeError.Case is not a separate special form. It is ordinary fallback-expression syntax whose fallback diverges:

var bytes = read(path) catch return LoadError.LoadFailed

Broad error collapse may be linted in stricter project profiles.

Top Error Handling

For the top Error type, use qualified cases in comparisons. Error is not finite and does not supply leading-dot shorthand context:

read_any(path) catch as err {
  if err == AllocError.OutOfMemory {
    diag.err(err)
  }

  return err
}

Checked narrowing from Error down to a closed error set is not part of V1.

Deferred Error Cleanup

V1 has no errdefer form. Error-only cleanup modeled after Zig's errdefer is tracked by CEP-0065: Error-Only Defer.