Errors
A function that can fail returns a Result, a value that can be missing is an Option, and the caller handles both with match, the question mark or a fallback.
There is no null and there are no exceptions. A value that can be missing and a call that can fail both say so in
their type, and the compiler makes the caller handle it.
Goal#
At the end of this page you can write a function that can fail, handle what it returns, and pass a failure on with
?.
A function that can fail#
type ConfigError {
case Missing(key: String)
case Invalid(key: String, reason: String)
}
fn readPort(settings: Map<String, String>): Result<Int, ConfigError> {
const raw = settings.get("port").okOr(ConfigError.Missing("port"))?
const port = Int.tryFrom(raw).mapError({ _ => ConfigError.Invalid "port", "not a number" })?
if port < 1 || port > 65535 {
return Fail ConfigError.Invalid("port", "out of range")
}
port
}
match readPort(["port": "80a"]) {
Ok(port) => print "port {port}"
Fail(.Missing(key)) => print "{key} is missing"
Fail(.Invalid(key, reason)) => print "{key} is invalid: {reason}"
}
// prints port is invalid: not a numberResult<Int, ConfigError> is either Ok with an Int or Fail with a ConfigError. A match handles both, as it
handles the cases of any type. settings.get returns a String?, and okOr turns a missing value into a Fail.
The body ends in port, not Ok(port): the value is wrapped for you.
Pass a failure on with ?#
? after a call takes the value out of an Ok. On a Fail it returns that Fail from the function at once. The
first readPort uses it twice. When the error types differ, ? converts the error, provided the target type says
how:
type ConfigError {
case Missing(key: String)
}
type AppError {
case Config(cause: ConfigError)
case Startup(message: String)
}
fn readPort(settings: Map<String, String>): Result<Int, ConfigError> {
if settings.isEmpty() {
return Fail ConfigError.Missing("port")
}
8080
}
fn start(settings: Map<String, String>): Result<Void, AppError> {
const port = readPort(settings)?
print "listening on {port}"
Ok void
}
match start([:]) {
Ok(_) => print "started"
Fail(error) => print "failed: {error}"
}
// prints failed: Config(cause: Missing(key: "port"))AppError.Config is the one case that holds a ConfigError, so ? wraps a ConfigError into it by itself.
A value that can be missing#
const ages = ["Ada": 36]
const age = ages.get("Grace") ?? 0
print age
print ages.get("Ada")
// prints 0
// prints Some(36)Int? is short for Option<Int>: either Some with a value or None. ?? gives a fallback for None, and ?.
reaches into the value if there is one: findUser(2)?.name.
A panic is for bugs#
fn percentageOf(part: Int, total: Int): Int {
if total == 0 {
panic "total must not be zero"
}
part * 100 / total
}
print percentageOf(1, 4)
// prints 25panic stops the program with exit code 101, and nothing can catch it. Use it for a state that should be impossible.
Bad input is an expected failure, and an expected failure is a Fail.
Next#
- Collections and pipelines - lists, maps and sets, and working through them.
- Result - everything you can do with
OkandFail. - The question mark operator - the exact rule, and when it converts.