Error Handling
Sun signals failure with exceptions. A function that can fail says so in its
signature, throw raises an error object, and the error unwinds the stack to
the nearest matching catch. There is no cost on the success path: a throwing
function returns its plain value like any other, and the unwinding machinery
only runs when something is actually thrown.
Error Types
An error is a class that implements the builtin IError interface:
interface IError {
function code() i32;
function message() String;
}Define your own by implementing both methods. Because an error is an ordinary class, it can carry whatever fields and extra methods you need:
class ParseError implements IError {
var line_: i32;
function init(line: i32) { this.line_ = line; }
function code() i32 { return 1; }
function message() String { return String("bad input"); }
function line() i32 { return this.line_; }
}The standard library (using sun;) provides Error(code, message) plus a set
of ready-made ones: EmptyError, NotFoundError, IndexOutOfBoundsError,
DivisionByZeroError, OverflowError, InvalidArgumentError, IOError,
AllocationError and ConversionError. Error accepts its message as a literal or as a String
built at runtime, and keeps its own copy.
Throwing Functions
A function that can fail declares it with the , IError suffix after its
return type, and raises an error with throw. throw takes an error object,
never a bare number:
function divide(a: i32, b: i32) i32, IError {
if (b == 0) { throw DivisionByZeroError(); }
return a / b;
}A function without the suffix cannot throw. Calling a throwing function is
only allowed from inside a try block or from another throwing function, so
an error either gets handled or is visibly passed up the signature chain — it
can never escape unnoticed:
// Propagates: no try needed, the error keeps unwinding to the caller.
function half_of_quotient(a: i32, b: i32) i32, IError {
return divide(a, b) / 2;
}Handling Errors with try/catch
try is block-form only. Each catch names a binding and its type, and the
clauses are tested in order against the concrete type of the thrown error:
function main() i32 {
try {
var r = divide(10, 0);
return r;
} catch (e: DivisionByZeroError) {
return -1; // this exact error type
} catch (e: IError) {
return e.code(); // anything else
}
}catch (e: IError)matches every error; a clause after it is unreachable and rejected.catch (e: SomeError)matches only that class. The binding is the real object, so methods unique to that class (likeline()above) are callable.- When no clause matches, the error keeps unwinding to the enclosing
tryor caller. - Inside a catch clause,
e.code()ande.message()dispatch to the thrown class.message()returns an ownedStringthat stays valid after the handler ends.
Inside a throwing function, integer division and modulo by zero throw
instead of crashing; catch (e: IError) catches them.
Cleanup During Unwinding
Unwinding runs the same cleanup a normal scope exit would: every live value
between the throw and the catch is dropped, so containers, strings and
other owning types release their memory exactly as if the function had
returned. Code before the failing call in a try block has run; code after it
has not.
How It Works
Errors are native LLVM exceptions, using the same personality routine and
landing pads as C++. A throwing function has the plain return type T in the
generated code; throw allocates the error, packs it with its IError vtable
and a type id, and unwinds. Each catch clause compares the type id to select
a handler, and cleanup landing pads drop live values on the way out. An error
that is never caught terminates the program.
Best Practices
- Handle or declare: wrap the call in
try, or add, IErrorto your own signature and let the caller decide. - Catch what you can handle: use a typed
catchfor the errors you have a real answer for, and let the rest propagate. - Throw meaningful errors: pick a stdlib error type or define a class that carries the details the handler needs.