Error handling¶
Candela separates the mistakes it can see from the ones it cannot. Type errors, unknown names, and malformed syntax stop the program before it starts. What remains at runtime is raised as an error you can catch.
Compile errors¶
The compiler checks the whole program, including imported modules, before
running anything. A type mismatch, a call to an undeclared function, a missing
main, or a value used outside its scope is reported with the offending source
line and nothing runs. Fixing these is part of writing the program rather than
part of handling failure.
Runtime errors¶
A runtime error carries a kind: a short lower-case string naming what went wrong. The common ones come from conversions and lookups.
invalid_int,invalid_float,invalid_bool: a conversion whose input does not parse.index_out_of_bounds,slice_out_of_bounds: a list or string index past the end.unknown_map_key:m[k]on a key the map does not hold.division_by_zero,modulo_by_zero.json_parse_error: malformed input tojson::parse.fs_not_found,fs_permission_denied, and the otherfs_kinds: file system calls.
An error that nobody catches stops the program and prints a report pointing at the expression that raised it.
try and catch¶
Wrap the risky work in try and handle the failure in catch. A catch with a
quoted kind handles that kind only; a catch naming a variable handles anything
and binds the kind to that name. A try needs at least one catch. A quoted
kind that no built-in error carries and nothing in the program throws is
reported as a warning, since a misspelt kind never matches; see
the error catalogue.
fn main() {
let zero = 0;
try {
print(10 / zero);
} catch "division_by_zero" {
print("cannot divide by zero");
}
try {
let xs = [1];
print(xs[9]);
} catch e {
print("failed: " + e);
}
}
Several catch blocks may follow one try. The catch-all comes last and takes
whatever the earlier ones did not.
fn main() {
try {
print(int("not a number"));
} catch "invalid_int" {
print("that was not a number");
} catch e {
print("something else: " + e);
}
}
A try covers everything the block runs, not only the expressions written in
it. An error raised inside a function the block calls unwinds to the enclosing
catch however many frames down it started, and the calls in between are
abandoned. That applies to your own functions and to the standard library
modules written in candela.
fn withdraw(balance, amount) {
if amount > balance {
throw "insufficient_funds";
}
return balance - amount;
}
fn checkout(balance) {
return withdraw(balance, 50);
}
fn main() {
try {
print(checkout(10));
} catch "insufficient_funds" {
print("declined");
}
}
Recovering usually means producing a fallback value, which reads best as a small function.
fn parse_or(text, fallback) {
let value = fallback;
try {
value = int(text);
} catch e {
value = fallback;
}
return value;
}
fn main() {
print(parse_or("42", 0), parse_or("nope", 0));
}
Raising your own¶
throw "kind"; raises an error with the kind you give it, so your own failures
catch exactly like the built-in ones. The kind is any string expression.
fn main() {
let balance = 10;
let amount = 50;
try {
if amount > balance {
throw "insufficient_funds";
}
print(balance - amount);
} catch "insufficient_funds" {
print("declined");
}
}
Expected absence and failure¶
An error is for the unexpected. When a value may reasonably be missing, or an
operation may reasonably fail, return it in the type instead: Option for a
value that may be absent, Result for an operation that may fail. Both are
enums from the standard library's prelude, so they need no import, and the
caller handles them with a match and cannot forget the failing case. The
lookups that may find nothing answer an Option this way: m.get(k),
xs.first(), xs.max().
fn divide(a, b) {
if b == 0 {
return Err("division by zero");
}
return Ok(a / b);
}
fn main() {
match divide(10, 2) {
Ok(value) => { print(value); }
Err(message) => { print(message); }
}
print(divide(1, 0).unwrap_or(0));
}
unwrap on a None or an Err raises, which is the deliberate way to say a
case cannot happen. See Enums for matching them.
Passing a failure on¶
x? after an Option or a Result reads the value out of a Some or an Ok.
On a None or an Err it returns that value from the function it is written
in, at once, so the caller gets the failure:
fn parse(text: string) -> Result<int, string> {
if text.parse<int>() is Some(n) {
return Ok(n);
}
return Err("not a number: " + text);
}
fn sum(a: string, b: string) -> Result<int, string> {
let x = parse(a)?;
let y = parse(b)?;
return Ok(x + y);
}
fn main() {
print(sum("1", "2"));
print(sum("1", "two"));
}
The function a ? is in has to return the same enum as its operand: an
Option for a ? on an Option, and for a Result one whose error type is
the same. The success side does not have to match, since a None or an Err
carries none of it. A function with no return annotation that uses ? returns
that enum. A ? anywhere else is a compile error: in main, which has no
caller to hand the failure to, or in a function that returns something else.
Read the value with match or unwrap_or there instead.
Assertions¶
The assert module raises when a condition does not hold, which is how the
standard library's own tests fail loudly.
import "std/assert";
fn main() {
assert::is_true(1 == 1);
assert::eq(2, 2);
print("checks passed");
}
A failed assertion raises the message it prints, so it stops the program on its
own and a try around the check catches it like any other error. That is how a
test harness reports which check failed instead of ending the run.