assert
The most basic way to "handle" errors is with !
. If an error occurs the hare
runtime will crash the program. In non-demo programs it should only be used when
an error is impossible.
match
A more elegant way to handle errors is with match
. Take the os::create
function for example:
fn create(str, fs::mode, fs::flags...) (io::file | fs::error);
This function can return one of two possible values: an io::file
if the file
was successfully opened, or fs::error
if not. The fs::error
type itself is a
tagged union which represents each of the errors that can be caused during file
operations.
A match
expression takes an expression between it's parenthesis which can
return one of several types from a tagged union, and each case handles one of
these types.
const file = match (os::create(path, 0o644, oflags)) {
case let file: io::file =>
yield file;
case errors::noaccess =>
fmt::fatalf("Error opening {}: Access denied", path);
case let err: fs::error =>
fmt::fatalf("Error opening {}: {}", path, fs::strerror(err));
};
defer io::close(file)!;
The first case in this example is the successful path. The second case handles a
specific error: errors::noaccess
, and the third handles any other errors.
The case let
syntax is used to bind the value for each case to a variable. In
the first branch, this value uses the yield
keyword to yield it to the parent
expression, which causes it to be "returned", in a manner of speaking, from the
match expression. This assigns the desired io::file
value to the file
variable.
The third case binds fs::error
to an err
variable, then passes it into
fs::strerror
to convert it to a string that can be presented to the user in an
error message. This is a standard pattern in hare: most modules provide a
strerror
function which stringifies all of the errors which can be caused by
that module.
propagate
use errors;
use fmt;
use fs;
use fs::{flags};
use io;
use os;
use strings;
export fn main() void = {
const path = os::args[1];
match (writehello(path)) {
case void =>
yield;
case let err: fs::error =>
fmt::fatalf("Error writing {}: {}", path, fs::strerror(err));
case let err: io::error =>
fmt::fatalf("Error writing {}: {}", path, io::strerror(err));
};
};
fn writehello(path: str) (fs::error | io::error | void) = {
const oflags = flags::WRONLY | flags::TRUNC;
const file = os::create(path, 0o644, oflags)?;
defer io::close(file)!;
const buf = strings::toutf8("Hello World\n");
io::write(file, buf)?;
};
It is cumbersome to use match
to enumerate every possible failure for every
function that might fail. To make it easier to deal with errors, the ?
operator is generally useful. The purpose of this operator is to check for
errors and, if found, return them to a higher call frame. If there are no
errors, execution will proceed normally.
To use this functionality, it is necessary to establish some error handling code somewhere in the program. In this sample, the "main" function is responsible for all error handling. In more complex programs, you may handle various kinds of errors at different levels throughout the program. An HTTP server, for instance, might have some logic to handle configuration errors by printing a message and stopping the server, but could handle errors related to a specific client by sending them an error responce or disconnecting them.
os::create
and io::write
together can return either an fs::error
or an
io::error
. We can then use ?
to return these errors immediately and extract
the useful types from the return values of these functions, and handle both
cases in "main".
define errors
use bufio;
use fmt;
use io;
use os;
use strconv;
use strings;
export fn main() void = {
match (prompt()) {
case void =>
yield;
case let err: error =>
fmt::fatal(strerror(err));
};
};
// An invalid number was provided.
type invalid = !(strconv::invalid | strconv::overflow);
// An error which indicates that io::EOF was unexpectedly encountered.
type unexpectedeof = !void;
// Tagged union of all possible errors.
type error = !(io::error | invalid | unexpectedeof);
// Converts an error into a user-friendly string.
fn strerror(err: error) str = {
match (err) {
case invalid =>
return "Expected a positive number";
case unexpectedeof =>
return "Unexpected end of file";
case let err: io::error =>
return io::strerror(err);
};
};
fn prompt() (void | error) = {
fmt::println("Please enter a positive number:")!;
const num = getnumber()?;
fmt::printfln("{} + 2 is {}", num, num + 2)!;
};
fn getnumber() (uint | error) = {
const num = match (bufio::scanline(os::stdin)?) {
case io::EOF =>
return unexpectedeof;
case let buf: []u8 =>
yield strings::fromutf8(buf);
};
defer free(num);
return strconv::stou(num)?;
};
Here we have a somewhat more complex sample in which we prompt the user to enter a number and then enumerate all possible error cases, such as entering something other than a number or pressing Ctrl+D to close the input file without entering anything at all.
We define new error types using !
to prefix the type declaration. invalid
is
an error type which derives from the errors which can be returned from
strconv::stou
, and unexpectedeof
is a custom error based on the void
type.
The latter does not store any additional state other than the type itself, so it
has a size of zero. We also define a tagged union containing each of these error
types, plus io::error
, also using !
to indicate that it is an error type.
We can return our custom error from getnumber
upon encountering io::EOF
(which is not normally considered an error) from bufio::scanline
, which is
propagated through prompt to main
. If an I/O error were to occur here, it
would be propagated similarly.
We can use any type as an error type if we wish. Some errors are an int
or
enum
type containing an error code, or a struct
with additional information
like a client IP address or a line and column in a file, and so on. We can also
provide out own strerror
function to provide a helpful error string, possibly
incorporating information stored in the error type.