Error Handling
try and catch
Runtime errors, including those from built-ins like http.get(), are
caught with try/catch:
fun parsePort(s) {
try {
return int(s);
} catch e {
print("bad port: " + e.message);
return 80;
}
}
print(parsePort("8080")); // 8080
print(parsePort("http")); // 80
The second call first prints bad port: int() cannot parse "http" as an integer in base 10.
The catch variable is optional:
let mut queue = [];
try { queue.pop(); } catch { print("queue is empty"); } // queue is empty
Error Types
An error raised by the language is a struct of one of the built-in error
types, each with fields { message, line = nil, cause = nil }:
TypeError— a value of the wrong type (1 + nil, calling a number)ArgumentError— the wrong number of argumentsValueError— the right type but a bad value (step 0,[].pop())IndexError— an index out of boundsKeyError— a missing property or struct fieldNameError— an undefined variableIOError— a file system call or anhttprequest failing; it also haspathandcode(see fs)
try {
[].pop();
} catch e {
print(type(e)); // ValueError
print(e.message); // array.pop() got an empty array.
print(e.line); // 2
}
A stack overflow is fatal and cannot be caught, and os.exit is not an
error at all: try does not stop it.
Catching by Type
catch e is T | U catches only the types it names, the same
test as v is T. Clauses are tried in
order, and an error none of them catches goes on up with its trace intact.
A clause without types catches everything and must come last.
let config = { port: 8080 };
fun get(key) {
try {
return config[key];
} catch e is KeyError {
return nil;
}
}
print(get("port")); // 8080
print(get("host")); // nil
fun load(raw) {
try {
return json.parse(raw).port + 1;
} catch e is KeyError | IndexError {
return "missing: " + e.message;
} catch e is ValueError {
return "invalid JSON";
} catch e {
return type(e);
}
}
print(load(r"""{"port": 80}""")); // 81
print(load("\{}")); // missing: Undefined property 'port'.
print(load("not json")); // invalid JSON
print(load(r"""{"port": "80"}""")); // TypeError
throw
throw raises a struct instance, built-in or your own; catch receives it
unchanged, with all its fields.
struct AgeError { message, value }
fun parseAge(v) {
if v < 0 throw AgeError { message: "negative age", value: v };
return v;
}
try {
parseAge(-1);
} catch e is AgeError {
print(e.value); // -1
}
An uncaught throw stops the program like any runtime error, reported as
Name: message when the struct has a string message:
struct AgeError { message, value }
throw AgeError { message: "negative age", value: -1 }; // AgeError: negative age
throw is also an expression, whose operand extends as far as it can, so it
fits where a value is expected: after ??, in ? :, or in a match arm.
struct ConfigError { message, key }
fun require(cfg, key) {
return cfg?.[key] ?? throw ConfigError { message: "missing " + key, key };
}
fun positive(n) {
return n > 0 ? n : throw ValueError { message: "not positive: {n}" };
}
let cfg = { port: 8080 };
print(require(cfg, "port")); // 8080
try { require(cfg, "host"); } catch e is ConfigError { print(e.message); } // missing host
try { positive(-2); } catch e is ValueError { print(e.message); } // not positive: -2
Wrap a lower-level error in cause to keep its details:
fun readConfig(raw) {
try {
return json.parse(raw);
} catch e {
throw ValueError { message: "bad config", cause: e };
}
}
try {
readConfig("port=80");
} catch e {
print(e.message); // bad config
print(type(e.cause)); // ValueError
}
Throwing anything but a struct is a TypeError:
throw "oops"; // TypeError: Can only throw struct instances.