Error Handling
Error unions, try, throw, catch arms, and fallbacks.
Zynx uses error-union types for recoverable failures. T throws(ErrorSet)
means an expression produces either a T or an error from ErrorSet. Multiple
error sets are written as an explicit union, such as
T throws(IOError | ParseError).
error ParseError {
Empty
}
fn parse_value(text: str) throws(ParseError) -> i32 {
if text == "" {
throw ParseError.Empty;
}
return 42;
}
Errors come from named error sets, and variants are referenced through the set
namespace, such as ParseError.Empty. Use error unions when the caller is
expected to handle failure. Use ordinary control flow for local choices that
are not failures.
Functions put thrown errors before the return arrow:
fn read() throws(IOError) -> usize
fn load() throws(IOError | ParseError) -> Data
A bare fn main() or async fn main() is the entry-point shorthand that can
use try without listing every propagated error; the runtime prints any
propagated error and exits nonzero. Most examples use bare main for this
reason. Library functions should still list their thrown error sets.
try unwraps the success value from an error-union expression. If the
expression is an error, that error is propagated from the current
error-returning function.
fn wrapper(text: str) throws(ParseError) -> i32 {
let value = try parse_value(text);
return value + 1;
}
try may propagate only error sets that are included in the current function's
declared error set. Bare main uses an internal dynamic destination set for
this rule.
A fallible call cannot be left as a bare statement. Handle it with try,
catch, a binding, or return propagation.
import std.io;
fn main() {
try io.stdout.write("hello\n");
}
If the current function is not fallible, use catch:
import std.io;
fn log() {
io.stdout.write("hello\n") catch {
_ => {}
}
}
return expr; returns a success value. Use throw ErrorSet.Variant; to exit
with an error value. Returning an error variant with return is rejected.
error FileError {
NotFound
}
fn load(fail: bool) throws(FileError) -> i32 {
if fail {
throw FileError.NotFound;
}
return 42;
}
Error variants can carry a message string at the throw site:
throw FileError.NotFound("missing config");
If no message is supplied, the runtime provides a default diagnostic string.
catch handles an error with a match-like arm block and produces a replacement
success value. Use _ when every error case should use the same fallback.
fn main() {
let value = parse_value("") catch { _ => 0 };
_ = value;
}
The fallback expression must match the success type. In the example,
parse_value has type i32 throws(ParseError), so the fallback is an i32.
Catch arms match error variants with a leading dot. For statically known error sets, the arms are exhaustiveness checked unless a wildcard arm is present. Use a block arm when handling needs more than one expression.
import std.os;
error FileError {
NotFound,
AccessDenied
}
fn load(fail: bool) throws(FileError) -> i32 {
if fail {
throw FileError.NotFound;
}
return 42;
}
fn main() {
let value = load(true) catch {
.NotFound => {
_ = os.write("not found\n");
return 0;
}
.AccessDenied => 1
};
_ = os.write("value: {value}\n");
}
return inside a catch arm block returns the fallback value for the catch
expression, similar to a match arm block. It does not return from the
enclosing function.
Warning
This differs from C, Rust, Zig, Go, and many expression languages. Inside a
catchexpression arm,return 0;means "this arm yields0". To return from the enclosing function, move thecatchto statement form or return after the catch expression.
When a fallible call is used as a standalone statement, catch can be a
statement block. Do not add a semicolon after the statement-form catch block.
import std.os;
error WriteError {
Failed
}
fn fail() throws(WriteError) -> void {
throw WriteError.Failed;
}
fn main() {
fail() catch {
.Failed => {
_ = os.write("handled\n");
}
}
}
Bind the message payload in the matching catch arm with .Variant(msg). The
message binding has type str.
import std.os;
error LoginError {
Rejected
}
fn login() throws(LoginError) -> void {
throw LoginError.Rejected("bad token");
}
fn main() {
login() catch {
.Rejected(msg) => {
_ = os.write("message: {msg}\n");
}
}
}
Use _(msg) when only a wildcard message binding is needed.
run() catch {
_(msg) => {
_ = os.write("error: {msg}\n");
}
}
Dynamic or unknown error sets from internal dynamic-loading experiments can only be handled with wildcard catch arms because normal source cannot prove concrete variant names.
Error unions are not boolean conditions. Handle them explicitly with try or
catch before using the success value.
import std.os;
error MathError {
DivisionByZero
}
fn divide(a: i32, b: i32) throws(MathError) -> i32 {
if b == 0 {
throw MathError.DivisionByZero;
}
return a / b;
}
fn main() {
let value = divide(10, 0) catch { _ => 0 };
if value == 0 {
_ = os.write("division failed\n");
return;
}
_ = os.write("value: {value}\n");
}
The same rule applies to ternary conditions: write the fallible operation
outside the condition, then branch on a plain bool or integer-like value.
A fallible function can return a plain success value or throw an error variant directly. Returning another fallible expression requires explicit handling.
fn parse_id(text: str) throws(ParseError) -> i32 {
return try parse_value(text);
}
Use catch instead when the wrapper wants to translate or replace the error.
await composes with error unions. Use try await to propagate the awaited
error, or catch the awaited value with parentheses.
import std.os;
error FileError {
NotFound
}
async fn read_id(fail: bool) throws(FileError) -> i32 {
if fail {
throw FileError.NotFound;
}
return 7;
}
async fn plus_one(fail: bool) throws(FileError) -> i32 {
let value = try await read_id(fail);
return value + 1;
}
async fn main() {
let ok = (await plus_one(false)) catch { _ => 0 };
let missing = (await plus_one(true)) catch {
.NotFound => {
return 99;
}
};
_ = os.write("{ok}-{missing}\n");
}
For structured futures that return error unions, the same rule applies:
let value = (await future.timeout(work(), duration)) catch { _ => fallback };