Skip to content

Advanced Error Handling

Optionals are only defined in certain code

✅ Variable declarations

int! example = unreliable_function();

✅ Function return signature

fn int! example() { /* ... */ }

Handling an empty Optional

File reading example

  • If the file is present the Optional result will be the first 100 bytes of the file.
  • If the file is not present the Optional Excuse will be IoError.FILE_NOT_FOUND.

Try running this code below with and without a file called file_to_open.txt in the same directory.

import std::io;
<*
Function modifies 'buffer'
Returns an Optional with a 'char[]' result
OR an empty Optional with an Excuse
*>
fn char[]! read_file(String filename, char[] buffer)
{
// Return Excuse if opening a file failed, using Rethrow `!`
File file = file::open(filename, "r")!;
// At scope exit, close the file.
// Discard the Excuse from file.close() with (void) cast
defer (void)file.close();
// Return Excuse if reading failed, using Rethrow `!`
file.read(buffer)!;
return buffer; // return a buffer result
}
fn void! main()
{
char[] buffer = mem::new_array(char, 100);
defer free(buffer); // Free memory on scope exit
char[]! read_buffer = read_file("file_to_open.txt", buffer);
// Catch the empty Optional and assign the Excuse
// to `excuse`
if (catch excuse = read_buffer)
{
io::printfn("Excuse found: %s", excuse);
// Returning Excuse using the `?` suffix
return excuse?;
}
// `read_buffer` behaves like a normal variable here
// because the Optional being empty was handled by 'if (catch)'
// which automatically unwrapped 'read_buffer' at this point.
io::printfn("read_buffer: %s", read_buffer);
}

Return a default value if Optional is empty

The ?? operator allows us to return a default value if the Optional is empty.

import std::io;
fn void test_bad()
{
int regular_value;
int! optional_value = function_may_error();
// An empty Optional found in optional_value
if (catch optional_value)
{
// Assign default result when empty.
regular_value = -1;
}
// A result was found in optional_value
if (try optional_value)
{
regular_value = optional_value;
}
io::printfn("The value was: %d", regular_value);
}
fn void test_good()
{
// Return '-1' when `foo_may_error()` is empty.
int regular_value = foo_may_error() ?? -1;
io::printfn("The value was: %d", regular_value);
}

Modifying the returned Excuse

A common use of ?? is to catch an empty Optional and change the Excuse to another more specific Excuse, which allows us to distinguish one failure from the other, even when they had the same Excuse originally.

import std::io;
fault NoHomework
{
DOG_ATE_MY_HOMEWORK,
MY_TEXTBOOK_CAUGHT_FIRE,
DISTRACTED_BY_CAT_PICTURES
}
fn int! test()
{
return IoError.FILE_NOT_FOUND?;
}
fn void! main(String[] args)
{
int! a = test(); // IoError.FILE_NOT_FOUND
int! b = test(); // IoError.FILE_NOT_FOUND
// We can tell these apart by default assigning our own unique
// Excuse. Our custom Excuse is assigned only if an
// empty Optional is returned.
int! c = test() ?? NoHomework.DOG_ATE_MY_HOMEWORK?;
int! d = test() ?? NoHomework.DISTRACTED_BY_CAT_PICTURES?;
// If you want to immediately return with an Excuse,
// use the "?" and "!" operators together, see the code below:
int! e = test() ?? NoHomework.DOG_ATE_MY_HOMEWORK?!;
int! f = test() ?? NoHomework.DISTRACTED_BY_CAT_PICTURES?!;
}

Force unwrapping expressions

The force unwrap operator !! will make the program panic and exit if the expression is an empty optional. This is useful when the error should – in normal cases – not happen and you don’t want to write any error handling for it. That said, it should be used with great caution in production code.

fn void find_file_and_test()
{
find_file()!!;
// Force unwrap '!!' is roughly equal to:
// if (catch find_file()) unreachable("Unexpected excuse");
}

Find empty Optional without reading the Excuse

import std::io;
fn void test()
{
int! optional_value = IoError.FILE_NOT_FOUND?;
// Find empty Optional, then handle inside scope
if (catch optional_value)
{
io::printn("Found empty Optional, the Excuse was not read");
}
}

Find empty Optional and switch on Excuse

if (catch) can also immediately switch on the Excuse value:

fn void! test()
{
if (catch excuse = optional_value)
{
case NoHomework.DOG_ATE_MY_HOMEWORK:
io::printn("Dog ate your file");
case IoError.FILE_NOT_FOUND:
io::printn("File not found");
default:
io::printfn("Unexpected Excuse: %s", excuse);
return excuse?;
}
}

Which is shorthand for:

fn void! test()
{
if (catch excuse = optional_value)
{
switch (excuse)
{
case NoHomework.DOG_ATE_MY_HOMEWORK:
io::printn("Dog ate your file");
case IoError.FILE_NOT_FOUND:
io::printn("File not found");
default:
io::printfn("Unexpected Excuse: %s", excuse);
return excuse?;
}
}
}

Run code if the Optional has a result

This is a convenience method, the logical inverse of if (catch) and is helpful when you don’t care about the empty branch of the code or you wish to perform an early return.

fn void test()
{
// 'optional_value' is a non-Optional variable inside the scope
if (try optional_value)
{
io::printfn("Result found: %s", optional_value);
}
// The Optional result is assigned to 'unwrapped_value' inside the scope
if (try unwrapped_value = optional_value)
{
io::printfn("Result found: %s", unwrapped_value);
}
}

Another example:

import std::io;
// Returns Optional result with `int` type or empty with an Excuse
fn int! reliable_function()
{
return 7; // Return a result
}
fn void! main(String[] args)
{
int! reliable_result = reliable_function();
// Unwrap the result from reliable_result
if (try reliable_result)
{
// reliable_result is unwrapped in this scope, can be used as normal
io::printfn("reliable_result: %s", reliable_result);
}
}

It is possible to add conditions to an if (try) but they must be joined with &&. However you cannot use logical OR (||) conditions:

import std::io;
// Returns Optional with an 'int' result or empty with an Excuse
fn int! reliable_function()
{
return 7; // Return an Optional result
}
fn void! main(String[] args)
{
int! reliable_result1 = reliable_function();
int! reliable_result2 = reliable_function();
// Unwrap the result from reliable_result1 and reliable_result2
if (try reliable_result1 && try reliable_result2 && 5 > 2)
{
// `reliable_result1` is can be used as a normal variable here
io::printfn("reliable_result1: %s", reliable_result1);
// `reliable_result2` is can be used as a normal variable here
io::printfn("reliable_result2: %s", reliable_result2);
}
// ERROR cannot use logical OR `||`
// if (try reliable_result1 || try reliable_result2)
// {
// io::printn("this can never happen);
// }
}

Shorthands to work with Optionals

Getting the Excuse

Retrieving the Excuse with if (catch excuse = optional_value) {...} is not the only way to get the Excuse from an Optional, we can use the macro @catch instead. Unlike if (catch) this will never cause automatic unwrapping.

fn void! main(String[] args)
{
int! optional_value = IoError.FILE_NOT_FOUND?;
anyfault excuse = @catch(optional_value);
if (excuse)
{
io::printfn("Excuse found: %s", excuse);
}
}

Checking if an Optional has a result without unwrapping

The @ok macro will return true if an Optional result is present and false if the Optional is empty. Functionally this is equivalent to !@catch, meaning no Excuse was found, for example:

fn void! main(String[] args)
{
int! optional_value = 7;
bool result_found = @ok(optional_value);
assert(result_found == !@catch(optional_value));
}

No void! variables

The void! type has no possible representation as a variable, and may only be a function return type.

To store the Excuse returned from a void! function without if (catch foo = optional_value), use the @catch macro to convert the Optional to an anyfault:

fn void! test()
{
return IoError.FILE_NOT_FOUND?;
}
anyfault excuse = @catch(test());