Error handling
leet offers utilities to deal with two groups of errors: recoverable and unrecoverable.
An error is recoverable when it can be treated by your code and execution is allowed to continue. For example if you’re making a REPL, getting a wrong instruction is not the end of the world: your code realizes it can’t execute it, lets the user know, and waits for the next instruction.
An error is unrecoverable when it is impossible to continue execution after it happens. For example if your code tries to access the 100th element of a slice that only has capacity for 10 elements, execution cannot continue.
Note
The separation between recoverable and unrecoverable errors is inspired by Rust’s error handling.
The error handling system is inspired by GLib’s error handling.
Unrecoverable errors
When you realize something went horribly wrong and your program cannot continue, you should exit with a panic. A panic must be triggered with a readable message describing the error.
Using our slice example from before:
void*
slice_sat(struct slice* p, size_t idx)
{
size_t offset = p->_el_size * idx;
if (offset >= p->_capacity)
panic("access out of bounds: slice capacity is %u but offset was %u.", p->_capacity, offset);
return p->_data + offset;
}
The panic will print your message to stderr along with where it happened, and exit with a non-zero code.
panicked at include/ds/slice.h:190:
slice access out of bounds: slice capacity is 10 but offset was 100.
Recoverable errors
Reporting errors
When a function fails with an error that does not require stopping execution, you may report the error to the caller so it can be handled. It is equivalent to a throw in languages that have exceptions. Functions that can report a recoverable error should accept a handle to an error pointer as the last parameter.
Using our REPL example:
void
bin_op(struct slice* s, char op, struct error** error)
{
if (s->_ptr < s->_el_size * 2)
{
error_set(error, LINVAL, "%c is a binary operation but the stack only has one value.", op);
}
else
{
// We have at least two values, execute the operation.
// ... snip ...
}
}
Attention
Reporting an error allocates memory and transfers its ownership to the caller. That means the caller must not allocate memory for the report, but must free the memory after handling it.
Handling errors
To get error reports you must pass a valid pointer to a function’s error parameter.
Calling a function with a valid error pointer and checking if the pointer was set is equivalent to a try-catch in languages that support exceptions.
The error pointer will be set if the function encounters an error, or left unchanged if the function executes successfully.
// ... snip ...
struct error* error = NULL;
bin_op(stack, op, &error);
if (error != NULL)
{
// Let the user know the operation could not be executed.
// ... snip ...
error_del(&error);
}
If an error can be safely ignored, you may call the function with a null pointer. It is equivalent to a try-catch with an empty catch block in languages that have exceptions. This does not mean the error will not be handled, it means the error can be handled by doing nothing.
bool
update(int id, struct error** error)
{
if (!exists(id))
{
set_error(error, LNOENT, "no entity with the given id: %d", id);
return false;
}
// ... snip ...
}
void
update_if_exists(int id)
{
update(id, NULL);
}
When nesting functions that can report errors it is important to not reuse the error pointer provided to the parent function, as it may be null. Instead you should create a temporary error pointer.
void
parent(struct error** error)
{
struct error* tmp_error = NULL;
child(&tmp_error);
if (tmp_error != NULL)
{
// Handle error from the child function.
// ... snip ...
// Call error_del or error_propagate.
}
}
Propagating errors
When a caller cannot handle an error from a nested function call, you may propagate the error upwards with error_propagate.
It is equivalent to calling a function that throws exceptions outside of a try-catch block or catching and re-throwing in languages that have exceptions.
When propagating an error you don’t need to worry about ownership, error_propagate is smart enough to free the error in cases where the caller chose to ignore it or transfer ownership otherwise.
parse, propagate it.bool
parse(const char* src, struct slice* tokens, struct error** error)
{
struct error* tmp_error = NULL;
// ... snip ...
parse_ident(src, ptr, tokens, &tmp_error);
if (tmp_error != NULL)
{
// We need an identifier but couldn't parse one,
// there's nothing we can do to fix that.
error_propagate(&tmp_error, error);
return false;
}
}
If all you need is to propagate an error and return, error_bubble can help avoid repeating the if statement from the previous example:
bool
parse(const char* src, struct slice* tokens, struct error** error)
{
struct error* tmp_error = NULL;
// ... snip ...
parse_ident(src, ptr, tokens, &tmp_error);
error_bubble(&tmp_error, error, false);
}
Performance
Reporting errors requires a memory allocation and string formatting.
For more performant reports you may avoid the string formatting with error_set_literal, or fallback to integer error codes.
TODO
Todo
Safe strings.
Todo
Warnings and assertions when error functions are used incorrectly.
API
#include <error.h>
Enums
Handle
-
struct error
Information about an error.
Functions
-
void error_set(struct error **p, enum error_code code, const char *format, ...)
Reports a recoverable error with a formatted message.
Functions that can fail should accept a handle to an error pointer as their last parameter.
This function creates an error and writes it to the given handle, transfering ownership to the caller. After handling the error, the caller must call error_del or error_propagate
If you don’t need a formatted string, use error_set_literal instead.
- Parameters:
p – Handle to write the error to.
error_code – High level description of the error.
format – Format string for the readable message.
... – Parameters for the format string.
-
void error_set_literal(struct error **p, enum error_code code, const char *message)
Reports a recoverable error with a message.
This is equivalent to error_set but uses a given message instead of formatting a string.
See also
- Parameters:
p – Handle to write the error to.
error_code – High level description of the error.
message – Readable message.
-
void error_del(struct error **p)
Deallocates the memory backing an error created by error_set or error_set_literal.
- Parameters:
p – Handle to the error to deallocate.
-
bool error_propagate(struct error **src, struct error **dst)
Propagates a recoverable error to the destination, if it happened.
When nesting functions that can fail, you must create a temporary error for the subfunction call (the error handle passed to the parent function could be
NULL).This function can propagate the error to the parent function, while also returning whether or not an error happened.
int can_error(struct error** error) { struct error* tmp_error = NULL; also_can_error(&tmp_error); if (error_propagate(&tmp_error, error)) // The sub function failed. // Possibly clean up, then return from this function. // The error propagates upward. return -1; // ... snip ... }
When all you need is to propagate the error and return immediately error_bubble may be used to simplify this pattern.
- Parameters:
src – The error to propagate.
dst – Handle to propagate the error to.
- Returns:
Whether or not an error occurred.
Macros
-
panic(...)
Reports an unrecoverable error and exits the program.
Exiting the program due to an unrecoverable error should be done through this macro. Should be called with a readable error message.
- Parameters:
format – Error message or format string.
... – Parameters for the format string.
-
error_bubble(src, dst, ret)
Propagates a recoverable error immediately, returning a value from the current function.
It is a common pattern to check if an error occurred, and return from the current function if it did. In some cases there’s no cleanup needed on the parent function and all you have to do is propagate the error and return. This macro turns that pattern into a single invocation.
From this:
int can_error(struct error** error) { struct error* tmp_error = NULL; also_can_error(&tmp_error); if (error_propagate(&tmp_error, error)) return -1; // ... snip ... }
To this:
int can_error(struct error** error) { struct error* tmp_error = NULL; also_can_error(&tmp_error); error_bubble(&tmp_error, error, -1); // ... snip ... }
If you don’t need to return a value, use error_bubble_void instead.
- Parameters:
src – Source error.
dst – Destination to propagate the error to.
ret – Return value of the function.
-
error_bubble_void(src, dst)
Propagates a recoverable error immediately, returning from the current function.
This is equivalent to error_bubble but doesn’t return a value.
See also
- Parameters:
src – Source error.
dst – Destination to propagate the error to.