Chapter 6 — Error handling
Coil has no exceptions. A function that can fail says so in its return type
by returning a Result, and the caller decides what to do with a failure.
Two short operators, ? and ??, keep that from getting verbose.
Result#
Result<T, E> is an enum from the prelude with two cases: Ok(value) when
the operation worked, and Err(error) when it did not. E is the type of
the error, often a string message:
use io::sync::{println};
use string::{format};
fn divide(int a, int b) -> Result<int, string> {
if b == 0 {
return Err("division by zero");
}
return Ok(a / b);
}
fn main() {
match divide(10, 2) {
Ok(q) => println(format("10 / 2 = %i", q))?,
Err(msg) => println("failed: " + msg)?,
}
match divide(1, 0) {
Ok(q) => println(format("1 / 0 = %i", q))?,
Err(msg) => println("failed: " + msg)?,
}
}
Output:
10 / 2 = 5
failed: division by zero
The caller cannot use the quotient without looking at which case it got, so an unhandled error cannot slip through unnoticed.
raise#
raise e returns Err(e) from the current function. It reads well for
validation:
fn check_port(int port) -> Result<int, string> {
if port < 1 || port > 65535 {
raise "port out of range";
}
return Ok(port);
}
? passes errors up#
Most of the time a function cannot fix an error itself and should hand it to
its caller. Postfix ? does exactly that: on Ok(v) it gives you v, and
on Err(e) it returns Err(e) from the current function straight away:
use io::sync::{println};
use string::{format};
use conv::{parse_int};
use text::{Searchable, trim};
// "3, 4, 5" → 12
fn sum_csv(string line) -> Result<int, string> {
let total = 0;
for field in line.split(",") {
total += parse_int(trim(field)?)?;
}
return Ok(total);
}
fn main() {
println(format("%v", sum_csv("3, 4, 5")))?;
println(format("%v", sum_csv("3, four, 5")))?;
}
Output:
Ok(12)
Err(invalid integer)
Without ?, each of those two calls would need its own match. This is
also why every println in this tutorial ends in ?: printing returns a
Result<int, IoError>, and ? passes a write failure up to main.
? works on Option too: in a function that returns an Option, x?
gives the value inside Some, or returns None.
One error type per function#
All the errors a function passes on with ? and raise must have the same
type. sum_csv above only deals in string errors, which is why it works.
main in that example only uses ? on println, whose errors are
IoErrors.
Mixing the two in one function is a compile error. The usual structure is a
main that only prints, calling functions that do the work and return their
own error type:
use io::sync::{println, eprintln};
use string::{format};
use conv::{parse_int};
fn run() -> Result<int, string> {
let width = parse_int("80")?;
let height = parse_int("24")?;
return Ok(width * height);
}
fn main() {
match run() {
Ok(cells) => {
println(format("%i cells", cells))?;
},
Err(e) => {
eprintln("error: " + e)?;
},
}
}
Output:
1920 cells
To turn one error type into another, use the map_err method:
parse_int(s).map_err(fn (string e) => MyError::BadNumber(e))?. Result and
Option have more methods like it (map, and_then, unwrap_or, …), all
listed on their reference pages: Result,
Option.
?? gives a fallback#
value ?? fallback takes the success value, or fallback when there is
none. It works on both Option and Result, and discards the error:
use io::sync::{println};
use string::{format};
use conv::{parse_int};
fn main() {
let retries = parse_int("not a number") ?? 3;
let names: Vec<string> = [];
let last = names.pop() ?? "nobody";
println(format("%i retries, last name %s", retries, last))?;
}
Output:
3 retries, last name nobody
Use ?? when a default is genuinely fine. When the user should hear about
the problem, use ? or match.
When main fails#
If main returns an error, the program stops with exit code 1 and prints
it:
fn main() {
raise "config file missing";
}
panic: main returned Err(config file missing) at app.hy:2:5
That is fine for scripts. For programs other people run, catch errors in
main and print a helpful message with eprintln, as run() above does.
panic is for bugs#
Some failures are not something the caller can handle: an index past the end
of a vector, an integer overflow, a broken assumption in your own code. These
panic: the program stops immediately with a message and the location.
defer blocks still run on the way out.
You can panic yourself:
fn percent(int part, int whole) -> int {
if whole == 0 {
panic "percent: whole must not be zero";
}
return part * 100 / whole;
}
The rule of thumb: return a Result when the failure depends on the outside
world (a missing file, bad user input, a network error), and panic when it
means the program itself is wrong.
Exercises#
- Write
fn parse_point(string s) -> Result<(int, int), string>that accepts text like"3,4", using.split,parse_intand?. - Change
divideto returnOption<int>instead, and use??to print0when dividing by zero. - Write a
mainthat callsparse_pointon a bad input and printserror: …to standard error.