kota's memex

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.