This section of the tutorial describes Austral’s approach to error handling.
We begin by describing what an error is, the different categories of errors, and how Austral handles each of them.
Following Sutter and Duffy, we divide errors into five categories, from most to least severe:
Physical Failure: Pulling the power cord, destroying part of the hardware.
Abstract Machine Corruption: A stack overflow.
Contract Violations: Due to a mistake the code, the program enters an invalid state. This includes:
An arithmetic operation leads to integer overflow or underflow (the contract here is that operands should be such that the operation does not overflow).
Integer division by zero (the contract is the divisor should be non zero).
Attempting to access an array with an index outside the array’s bounds (the contract is that the index should be within the length of the array).
Any violation of a programmer-defined precondition, postcondition, assertion or invariant.
These errors are bugs in the program. They are unpredictable, often happen very rarely, and can open the door to security vulnerabilities.
Memory Allocation Failure:
null, essentially. This gets its own category because allocation is pervasive, especially in higher-level code, and allocation failure is rarely modeled at the type level.
Failure Conditions. Things that aren’t actually errors, but are rather situations you would encounter normally and have to be explicitly handled in the code. “File not found”, “connection failed”, “directory is not empty”, “timeout exceeded”.
The different error categories should be approached differently.
For physical hardware failure, there is little we can do except write code that uses operating system APIs that provide ACID guarantees.
For abstract machine corruption, the solution is to crash the program. Attempting to recover from so fundamental a failure provides endless security vulnerabilities: it is likely that if the stack has overflown it is because of an attack.
Allocation failure and failure conditions are not errors. As in Rust and many modern languages, these should be modeled at the value level, using things like option types.
Finally, contract violations. These are bugs in the program: if there is unplanned arithmetic overflow, if there is a division by zero inside a data structure where that shouldn’t happen, if arrays are accessed out of bounds, that’s all an error in the program. And, importantly, bugs are not recoverable, because recovery introduces its own security vulnerability opportunities.
So Austral’s solution is to crash the program by aborting. The built-in
abort function lets you do this in your own custom situations:
function division(dividend: Int64, divisor: Int64): Int64 is if divisor = 0 then abort("Division by zero error in division()"); end if; return a / b; end;
Example: The Option Type
Option type lets you model places where a value can either
be present or not. It is defined like this:
union Option[T: Type]: Type is case None; case Some is value: T; end; end;
For example, imagine you have a linear type
Map[K, V]. The function that
retrieves a value by key might have this signature:
generic [K: Free, V: Type, R: Region] function get(mapref: &[Map[K, V], R], key: K): Option[V];
And you could use it like this:
let opt: Option[Int32] := get(&map, "postCount"); case opt of when Some(value: Int32) do print("postCount = "); printLn(value); when None do printLn("No value with the key `postCount`."); end case;
Example: The Either Type
Austral’s built-in either type can be used to represent a case where you get either an error value or a success value. It is defined like this:
union Either[L: Type, R: Type]: Type is case Left is left: L; case Right is right: R; end;