Reservations were advisory bookkeeping: any component could name any status, so the registry only detected declared-range overlap between components that both opted in. Naming a status now requires a reservation. akerr_register_status_name() checks that the range belongs to the caller, and the legacy two-argument akerr_name_for_status() set path, which cannot identify its caller, requires that some reservation covers the status. Every refusal is logged and names the real owner, because a name that fails to register degrades that code to "Unknown Error" in every later stack trace. Fix a reservation made before the first PREPARE_ERROR being silently discarded. akerr_init() clears the tables, so whichever component first triggered it wiped an earlier reservation and the next component to claim the same range was told it was free, producing exactly the undetected aliasing the registry exists to prevent. Every registry entry point now calls akerr_init(), which sets its guard before doing any work so those calls do not recurse. Replace the linear-scan name array with an open-addressed hash table, taking lookup from O(n) to O(1) and raising usable capacity from 512 entries (366 free to consumers after errno registration) to 3072 (~2900 free). Both table sizes are build-time overridable and applied PRIVATE: they live entirely in src/error.c, so raising them cannot desynchronize a library from its consumers the way AKERR_MAX_ERR_VALUE could. Exhausting either table is now logged and returned to the caller rather than silently dropping the entry. No dynamic allocation is introduced; both tables remain file-scope arrays, and the library's undefined-symbol set gains only strcmp and strlen. Register names for AKERR_EOF, AKERR_ITERATOR_BREAK and AKERR_NOT_IMPLEMENTED, which had none and rendered as "Unknown Error" in every stack trace carrying them. err_error_names.c now sweeps the whole AKERR_* offset span so a code added without a name fails there instead of in production traces. Add static assertions that the slot count is a power of two and that AKERR_BADEXC stays inside the library's own 0-255 band, the latter guarding against a host errno space large enough to push library codes into the range consumers are told to allocate from. Set a project version and soname (1.0.0 / libakerror.so.1) so a stale installed library can no longer be silently paired with newer headers, and so akerror.pc ships a real Version field instead of an empty one. Mutation testing surfaced an out-of-bounds probe in the new table that the suite did not catch: masking with SLOTS rather than SLOTS-1 indexes past the array, and err_maxval.c asserted only that some names registered before the table filled, which a collapsed probe sequence still satisfies. It now requires a substantial entry count and reads every entry back by its own distinct name. Tests: 28/28 pass. Coverage 99.4% line / 86.8% branch. Mutation score for src/error.c 74% -> 77.3%. Compatibility: source and ABI break. AKERR_MAX_ERR_VALUE and the __AKERR_ERROR_NAMES data symbol are gone, custom codes must move out of 0-255, and names must be registered against a reserved range. README.md carries the migration steps. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
614 lines
28 KiB
Markdown
614 lines
28 KiB
Markdown
# Summary
|
||
|
||
This library provides a TRY/CATCH style exception handling mechanism for C.
|
||
|
||

|
||
|
||
## Upgrade notice: custom status codes (1.0.0)
|
||
|
||
Version 1.0.0 replaces the consumer-sized status-name array with a private
|
||
registry, and makes status-code ownership explicit and enforced. This is a
|
||
source and ABI break. The library now carries a version and an soname
|
||
(`libakerror.so.1`), so a stale installed library can no longer be silently
|
||
paired with a newer header — but anything built against a pre-1.0.0 header must
|
||
be rebuilt.
|
||
|
||
What was removed:
|
||
|
||
* `AKERR_MAX_ERR_VALUE` — the registry is sparse and accepts any `int`, so
|
||
consumers no longer size it. Delete every compile definition and source
|
||
reference. A stale `-DAKERR_MAX_ERR_VALUE=...` is now harmless but useless.
|
||
* `__AKERR_ERROR_NAMES` — the name table is private to the library. Code that
|
||
touched this data symbol was using an undocumented interface; use
|
||
`akerr_name_for_status()`.
|
||
|
||
To migrate:
|
||
|
||
1. Rebuild libakerror and every dependent library against the new header.
|
||
2. Move custom status codes out of the reserved `0`–`255` band. Use fixed
|
||
integer constants beginning at `AKERR_FIRST_CONSUMER_STATUS` (256) rather
|
||
than offsets from `AKERR_LAST_ERRNO_VALUE`, so a libc that grows an errno
|
||
cannot move your codes.
|
||
3. Assign a distinct range to every library that may coexist in one process, and
|
||
coordinate those ranges at the application or dependency-stack level.
|
||
4. Reserve the complete range with `akerr_reserve_status_range()` during
|
||
initialization. Treat any result other than `AKERR_STATUS_RANGE_OK` as an
|
||
initialization failure.
|
||
5. Register names with `akerr_register_status_name()`, passing the same owner
|
||
string you reserved with. **You can no longer name a status you have not
|
||
reserved** — see "Ownership is enforced" below.
|
||
|
||
For example:
|
||
|
||
```c
|
||
enum {
|
||
MYLIB_ERR_BASE = AKERR_FIRST_CONSUMER_STATUS, /* 256 */
|
||
MYLIB_ERR_PARSE = MYLIB_ERR_BASE,
|
||
MYLIB_ERR_STORAGE,
|
||
MYLIB_ERR_LIMIT
|
||
};
|
||
|
||
#define MYLIB_OWNER "mylib"
|
||
|
||
int mylib_init(void)
|
||
{
|
||
if (akerr_reserve_status_range(MYLIB_ERR_BASE,
|
||
MYLIB_ERR_LIMIT - MYLIB_ERR_BASE,
|
||
MYLIB_OWNER) != AKERR_STATUS_RANGE_OK) {
|
||
return MYLIB_INIT_FAILED; /* another component owns part of the range */
|
||
}
|
||
|
||
akerr_register_status_name(MYLIB_OWNER, MYLIB_ERR_PARSE, "Parse Error");
|
||
akerr_register_status_name(MYLIB_OWNER, MYLIB_ERR_STORAGE, "Storage Error");
|
||
return MYLIB_INIT_OK;
|
||
}
|
||
```
|
||
|
||
You do not need to call `akerr_init()` first. Every registry entry point calls
|
||
it for you, so a library that reserves its range before anything else in the
|
||
process has touched libakerror keeps that reservation. (Before 1.0.0 this was
|
||
silently destructive: `akerr_init()` cleared the tables, so a reservation made
|
||
too early was discarded and the next component to claim the same range was told
|
||
it was free.)
|
||
|
||
### Ownership is enforced
|
||
|
||
Reserving a range is no longer advisory bookkeeping. A status may only be named
|
||
from inside a reservation:
|
||
|
||
* `akerr_register_status_name(owner, status, name)` requires that `status` fall
|
||
in a range reserved by `owner`. Naming another component's status fails with
|
||
`AKERR_STATUS_NAME_FOREIGN`, and naming a status nobody reserved fails with
|
||
`AKERR_STATUS_NAME_UNRESERVED`.
|
||
* The two-argument `akerr_name_for_status(status, name)` set path still works,
|
||
but it cannot identify its caller, so it can only require that *some*
|
||
reservation covers the status. Prefer the owned form: it is the one that
|
||
catches a component writing into a range that is not its own.
|
||
|
||
Every refusal is reported through `akerr_log_method` and names the real owner,
|
||
because a name that fails to register degrades that code to `"Unknown Error"` in
|
||
every later stack trace. Registration failures are not fatal by themselves —
|
||
check the return value during initialization if you want them to be.
|
||
|
||
This detects *name* collisions. It cannot detect two components compiling the
|
||
same integer into a `HANDLE` label without ever registering a name, so every
|
||
co-resident library should still reserve its range.
|
||
|
||
### Return codes
|
||
|
||
`akerr_reserve_status_range()`:
|
||
|
||
| Code | Meaning |
|
||
| --- | --- |
|
||
| `AKERR_STATUS_RANGE_OK` (0) | Range reserved, or an identical range was already reserved by the same owner |
|
||
| `AKERR_STATUS_RANGE_OVERLAP` | Part of the range is owned by someone else (logged, with the owner) |
|
||
| `AKERR_STATUS_RANGE_FULL` | No reservation slots remain |
|
||
| `AKERR_STATUS_RANGE_INVALID` | `count < 1`, NULL/empty/over-long owner, or the range overflows `int` |
|
||
|
||
`akerr_register_status_name()`:
|
||
|
||
| Code | Meaning |
|
||
| --- | --- |
|
||
| `AKERR_STATUS_NAME_OK` (0) | Name registered |
|
||
| `AKERR_STATUS_NAME_UNRESERVED` | No reservation contains this status |
|
||
| `AKERR_STATUS_NAME_FOREIGN` | The status is in a range owned by someone else |
|
||
| `AKERR_STATUS_NAME_FULL` | The name registry is full |
|
||
| `AKERR_STATUS_NAME_INVALID` | NULL/empty/over-long owner, or a NULL name |
|
||
|
||
Repeating an identical reservation for the same owner is a no-op. A *subset* or
|
||
*superset* of your own range is not — it is reported as an overlap. Reserve the
|
||
whole range in one call.
|
||
|
||
### Capacity
|
||
|
||
Both tables are fixed size, allocated in BSS, and never grow:
|
||
|
||
| Limit | Default | Build-time override |
|
||
| --- | --- | --- |
|
||
| Status names | 3072 usable (4096 slots, 75% load) | `-DAKERR_STATUS_NAME_SLOTS=<power of two>` |
|
||
| Reserved ranges | 64 | `-DAKERR_MAX_RESERVED_STATUS_RANGES=<n>` |
|
||
|
||
`akerr_init()` consumes one name slot per host errno value plus one per
|
||
`AKERR_*` code — around 150 on glibc, leaving roughly 2900 for all consumers in
|
||
the process to share. That figure varies with the host libc, so treat it as
|
||
approximate rather than a budget to fill.
|
||
|
||
Both overrides are CMake cache variables and apply `PRIVATE` to the library
|
||
target. The tables live entirely inside `src/error.c`, so raising them changes
|
||
nothing a consumer can observe — unlike the `AKERR_MAX_ERR_VALUE` they replaced,
|
||
where a mismatch between the library and its consumers corrupted memory. Set
|
||
them when configuring libakerror itself:
|
||
|
||
```sh
|
||
cmake -S . -B build -DAKERR_STATUS_NAME_SLOTS=16384
|
||
```
|
||
|
||
Exhausting either table is reported through `akerr_log_method` and returned to
|
||
the caller; it is never silent.
|
||
|
||
### Thread safety
|
||
|
||
The registry is process-global mutable state with no locking. Reserve ranges and
|
||
register names during single-threaded initialization, before spawning threads.
|
||
Lookups (`akerr_name_for_status(status, NULL)`) are safe to call concurrently
|
||
once registration has finished.
|
||
|
||
# Why?
|
||
|
||
There is nothing wrong with C as it is. This library does not claim to fix some problem with C.
|
||
|
||
Instead, this library implements a pragmatic and stylistic choice to assist the programmer in better handling errors in their programs. Vanilla C provides everything you need to do this out of the box, but this library makes it easier to avoid pointing certain guns at your foot, and when you do, it provides better context with those errors to help you more quickly recover.
|
||
|
||
Why? Because some programmers prefer to have the power of C with just a little bit of help in managing their errors.
|
||
|
||
# Library Architecture
|
||
|
||
## Philosophy of Use
|
||
|
||
This library has 6 guiding principles:
|
||
|
||
* Manually checking every possible return code for every possible meaning of that return code is tedious and prone to miss unpredicted failure cases
|
||
* Functions should return rich descriptive error contexts, not values
|
||
* Uncaught errors should cause program termination with a stacktrace
|
||
* Dynamic memory allocation is the source of many errors and should be avoided if possible
|
||
* Manipulating the call stack directly is error prone and dangerous
|
||
* Declaring, capturing, and reacting to errors should be intuitive and no more difficult than managing return codes
|
||
|
||
## Lifecycle of an error in the AKError library
|
||
|
||
TL;DR - `akerr_ErrorContext` objects are filled with error context information and bubbled up through nested control structures until they are handled or reach the top level, where an unhandled error halts program termination with a stack trace
|
||
|
||
1. At the point where an error occurs, an `akerr_ErrorContext` object is initialized and populated with information regarding the failure
|
||
2. The akerr_ErrorContext is returned from the scope where the error was detected
|
||
3. The akerr_ErrorContext enters a control structure provided by the AKError library through a series of macros that examine `akerr_ErrorContext` objects as they pass through
|
||
4. The control structure checks to see if the `akerr_ErrorContext` has an error set, and if so, if there are any handlers in the current control structure that can handle it
|
||
5. If the current control structure can handle the `akerr_ErrorContext`, it does so
|
||
6. If the current control structure can not handle the `akerr_ErrorContext`, then the current control structure's cleanup code (if any) is executed, and the `akerr_ErrorContext` object is passed out of the current control structure to the parent control structure
|
||
7. Steps 2-6 are repeated through as many control structures as are necessary to reach the first level of the control structure
|
||
8. When the first level of the control structure is reached, if the `akerr_ErrorContext` has an error set in it, then the stack trace information in the `akerr_ErrorContext` object is used to print a stack trace using the configured logging function, and program termination is halted
|
||
|
||
## What is in an Error Context
|
||
|
||
The Error Context object is a simple object which contains a few things:
|
||
|
||
* A numeric error code
|
||
* The name of the file in which the error occurred
|
||
* The name of the function in which the error occurred
|
||
* The line number in the file at which the error occurred
|
||
* A character buffer containing a message about the error in question
|
||
|
||
The structure also contains housekeeping information for the library which are of no specific interest to the user. See [include/akerror.h](include/akerror.h) for more details.
|
||
|
||
## What are the control structures
|
||
|
||
The library is structured around a series of macros that construct `switch` statements that perform logic against an `akerr_ErrorContext` which exists in the current scope and has been initialized. These macros must be assembled in a specific order to produce a syntactically correct `switch` statement which performs correct operations against the `akerr_ErrorContext` to attempt operations, detect failures, perform cleanup operations, handle errors, and then exit a given scope in a success or failure state.
|
||
|
||
## Functions and Return Codes
|
||
|
||
This library can catch errors from any function or expression that returns an integer value, or from functions that return `akerr_ErrorContext *`.
|
||
|
||
Any function which uses the `PREPARE_ERROR` macro should have a return type of `akerr_ErrorContext *`. The macros within this library, when they detect an unhandled error, will attempt to pass up the unhandled error to the context of the previous function in the call stack. This allows for errors to propagate up through the call stack in the same way as exceptions. (For example, if you use traditional C error handling in a call stack of `a() -> b() -> c()`, and `c()` fails because it runs out of memory, `b()` will likely detect that error and return some error to `a()`, but it may or may not return the context of what failed and why. With this, you get that context all the way up in `a()` without knowing anything about `c()`.
|
||
|
||
## Error codes
|
||
|
||
The library uses integer values to specify error codes inside of its context. These integer return codes are defined in `akerror.h` in the form of `AKERR_xxxxx` where `xxxxx` is the name of the error code in question. See `akerror.h` for a list of defined errors and their descriptions.
|
||
|
||
You can define additional error types as integer constants. Values 0 through 255
|
||
are reserved by libakerror (the host's errno values plus the `AKERR_*` codes);
|
||
consumers allocate codes starting at `AKERR_FIRST_CONSUMER_STATUS` (256). Status
|
||
names are stored sparsely, so any `int` is a legal status and no compile
|
||
definition is needed to use large values.
|
||
|
||
Every library that may coexist in one process must reserve its range during
|
||
initialization and treat any nonzero result as a startup error:
|
||
|
||
```c
|
||
#define MYLIB_OWNER "my-library"
|
||
|
||
if (akerr_reserve_status_range(256, 16, MYLIB_OWNER) != AKERR_STATUS_RANGE_OK) {
|
||
/* Refuse to initialize: another component owns part of the range. */
|
||
}
|
||
```
|
||
|
||
Reservations are process-local, fixed-capacity, and idempotent when the same
|
||
owner repeats the exact same range. They preserve compile-time integer constants
|
||
so `HANDLE` still works, since `case` labels require them.
|
||
|
||
Then name each code, quoting the owner you reserved with:
|
||
|
||
```c
|
||
akerr_register_status_name(MYLIB_OWNER, 256, "Some Error Code Description");
|
||
```
|
||
|
||
Naming a status outside your reservation is refused and logged. See
|
||
[the upgrade notice](#upgrade-notice-custom-status-codes-100) for the return
|
||
codes, the capacity limits and how to raise them, and thread-safety rules.
|
||
|
||
|
||
# Installation
|
||
|
||
```bash
|
||
cmake -S . -B build
|
||
cmake --build build
|
||
cmake --install build
|
||
```
|
||
|
||
## Templating and autogenerated code
|
||
|
||
The build process relies upon `scripts/generrno.sh` which performs the following:
|
||
|
||
1. Executes `errno --list` and gathers up the output
|
||
1. Templates `include/akerror.tmpl.h` into `include/akerror.h` to set the `AKERR_LAST_ERRNO_VALUE` equal to the highest integer defined by `errno`
|
||
2. Generates `src/errno.c` which contains a function called by `akerr_init` which initializes all of the status names for the previously defined values of `errno`.
|
||
|
||
## Dependencies
|
||
|
||
This library depends upon `stdlib`. If you don't want to link against stdlib, you must modify the library code to include headers and link against a library that provides the following:
|
||
|
||
- `memset` function
|
||
- `strncpy` function
|
||
- `strlen` function
|
||
- `strcmp` function
|
||
- `sprintf` function
|
||
- `exit` function
|
||
- `bool` type
|
||
- `NULL` type
|
||
- `INT_MAX` constant
|
||
- `PATH_MAX` constant
|
||
|
||
... then you can compile it thusly:
|
||
|
||
```
|
||
cmake -S . -B build -DAKERR_USE_STDLIB=OFF
|
||
cmake --build build
|
||
cmake --install build
|
||
```
|
||
|
||
# Using the library
|
||
|
||
## Setting up your project
|
||
|
||
Include it
|
||
|
||
```c
|
||
#include <akerror.h>
|
||
```
|
||
|
||
Link the library directly, or
|
||
|
||
```sh
|
||
cc -lakerror
|
||
```
|
||
|
||
Using pkg-config, or
|
||
|
||
```sh
|
||
pkg-config akerror --cflags
|
||
pkg-config akerror --ldflags
|
||
```
|
||
|
||
Using cmake:
|
||
|
||
```cmake
|
||
find_package(akerror REQUIRED)
|
||
pkg_check_modules(akerror REQUIRED akerror)
|
||
target_link_libraries(YOUR_TARGET PRIVATE akerror::akerror)
|
||
```
|
||
|
||
Using this project as a submodule with cmake:
|
||
|
||
```cmake
|
||
add_subdirectory(deps/libakerror EXCLUDE_FROM_ALL)
|
||
|
||
target_link_libraries(YOUR_PROJECT PRIVATE akerror::akerror)
|
||
```
|
||
|
||
|
||
## (Optional) Configuring the logging function
|
||
|
||
The default logging function (used for logging stack traces on failure) defaults to a wrapper that calls `fprintf(stderr, f, ...)`. If you want to override this behavior, then set the error handler to a function with a printf-style signature:
|
||
|
||
```
|
||
void my_logger(const char *fmt, ...)
|
||
{
|
||
/* ... do something */
|
||
}
|
||
|
||
|
||
/* set your custom error handler */
|
||
akerr_log_method = &my_logger;
|
||
|
||
/* proceed to use the library */
|
||
```
|
||
|
||
## Setting Up the Error Context
|
||
|
||
Before you can use any of these macros you must set up an error context inside of the current scope.
|
||
|
||
```c
|
||
PREPARE_ERROR(errctx);
|
||
```
|
||
|
||
This will create a akerr_ErrorContext structure inside of the current scope named `errctx` and initialize it. This structure is used for all operations of the library within the current scope. Attempting to use the library in a given scope before calling this will result in compile-time errors.
|
||
|
||
## Attempting an Operation
|
||
|
||
```c
|
||
ATTEMPT {
|
||
// ... code
|
||
} CLEANUP {
|
||
} PROCESS(errctx) {
|
||
} FINISH(errctx, true)
|
||
```
|
||
|
||
`ATTEMPT { ... }` is the block within which you will perform operations which may cause errors that need to be caught. See "Capturing errors", below.
|
||
|
||
`CLEANUP { ... }` is the block within which you will perform any code which MUST be executed REGARDLESS of whether or not errors were thrown. Closing open file handles, or releasing memory, for example.
|
||
|
||
`PROCESS(errctx) { ... }` is the block within which you will handle any errors that were caught inside of the `ATTEMPT` block. See "Handling Errors" below.
|
||
|
||
`FINISH(errctx, true)` terminates the attempt operation. The `FINISH` macro takes two arguments: the name of the akerr_ErrorContext, and a boolean regarding whether or not to pass unhandled errors up to the calling function. Unless you are inside of your `main()` method, this should be true. Inside of your `main()` method, call `FINISH_NORExbTURN(errctx)` instead.
|
||
|
||
|
||
# Capturing errors
|
||
|
||
Inside of an `ATTEMPT` block, any operation which could generate or represent an error should be wrapped in one of several macros.
|
||
|
||
## Capturing errors from functions which return akerr_ErrorContext *
|
||
|
||
For functions that return `akerr_ErrorContext *`, you should use the `CATCH` macro.
|
||
|
||
```c
|
||
ATTEMPT {
|
||
CATCH(errctx, errorGeneratingFunction())
|
||
} // ...
|
||
```
|
||
|
||
This will assign the return value of the function in question to the akerr_ErrorContext previously prepared in the current scope. If the function returns an akerr_ErrorContext that indicates any type of error, the `ATTEMPT` block is immediately exited, and the `CLEANUP` block begins.
|
||
|
||
(One caveat: because this exit is implemented with a C `break`, `CATCH` must not be used inside a loop within the `ATTEMPT` block — see the section "Important: do not use CATCH or FAIL_*_BREAK inside a loop" below.)
|
||
|
||
## Setting errors from functions or expressions returning integer
|
||
|
||
For functions that return integer, such as logical comparisons or most standard library functions, use the `FAIL_ZERO_BREAK` and `FAIL_NONZERO_BREAK` macros. These macros allow you to capture an integer return code from an expression or function and set an error code in the current context based off that return.
|
||
|
||
Here is an example of checking for a NULL pointer
|
||
|
||
```c
|
||
ATTEMPT {
|
||
FAIL_ZERO_BREAK(errctx, (somePointer == NULL), AKERR_NULLPOINTER, "Someone gave me a NULL pointer")
|
||
} // ...
|
||
```
|
||
|
||
Here is an example of checking for two strings that are not equal
|
||
|
||
```c
|
||
ATTEMPT {
|
||
FAIL_NONZERO_BREAK(errctx, strcmp("not", "equal"), AKERR_VALUE, "Strings are not equal")
|
||
} // ...
|
||
```
|
||
|
||
When either of these two macros are used, the `ATTEMPT` block is immediately exited, and the `CLEANUP` block begins.
|
||
|
||
## Important: do not use CATCH or FAIL_*_BREAK inside a loop
|
||
|
||
`CATCH`, `FAIL_ZERO_BREAK`, `FAIL_NONZERO_BREAK`, and `FAIL_BREAK` leave the `ATTEMPT` block by executing a C `break` statement. In C, `break` only exits the *innermost* enclosing `for`, `while`, `do`, or `switch`. Therefore **these macros must not be used inside a loop (or a nested `switch`) that is itself inside an `ATTEMPT` block.** If you do, the `break` escapes only the loop — not the `ATTEMPT` — and the rest of the `ATTEMPT` body then runs with an error already pending:
|
||
|
||
```c
|
||
ATTEMPT {
|
||
for ( int i = 0; i < n; i++ ) {
|
||
CATCH(errctx, process(items[i])); // WRONG: break exits the for loop, not the ATTEMPT
|
||
}
|
||
// ... this code still executes, with errctx already in an error state ...
|
||
} CLEANUP {
|
||
} PROCESS(errctx) {
|
||
} FINISH(errctx, true);
|
||
```
|
||
|
||
Note that moving the loop into a helper function does **not** fix this on its own — if the helper still wraps the loop in an `ATTEMPT` and uses `CATCH`/`FAIL_*_BREAK` inside it, it has the exact same problem. The fix is to iterate with `return`-based macros, which are unaffected by loop nesting. Use one of the two patterns below.
|
||
|
||
**Pattern 1 — use `PASS` (or a `FAIL_*_RETURN` macro) inside the loop.** These exit the *enclosing function* with a `return` rather than a `break`, so loop nesting is irrelevant. Use this when the loop should stop and propagate on the first error:
|
||
|
||
```c
|
||
akerr_ErrorContext AKERR_NOIGNORE *process_all(Item *items, int n)
|
||
{
|
||
PREPARE_ERROR(errctx);
|
||
for ( int i = 0; i < n; i++ ) {
|
||
PASS(errctx, process(items[i])); // returns from process_all on the first error
|
||
}
|
||
SUCCEED_RETURN(errctx);
|
||
}
|
||
```
|
||
|
||
**Pattern 2 — move the loop into a helper and `CATCH` the single call.** When you need a `CLEANUP` block or want to `HANDLE` the error locally, put the loop in its own `akerr_ErrorContext *`-returning function (written per Pattern 1) and `CATCH` that one call. The `CATCH` is then not inside a loop, so its `break` scopes to the `ATTEMPT` correctly:
|
||
|
||
```c
|
||
ATTEMPT {
|
||
CATCH(errctx, process_all(items, n)); // a single CATCH, not looped
|
||
} CLEANUP {
|
||
// ... always runs ...
|
||
} PROCESS(errctx) {
|
||
} HANDLE(errctx, AKERR_VALUE) {
|
||
// ... handle a failure from any iteration ...
|
||
} FINISH(errctx, true);
|
||
```
|
||
|
||
# Passing errors
|
||
|
||
Sometimes you can't actually do anything about the errors that come out of a given method, but you want that error to be propagated back up the call chain, and to be properly reported. If this is your goal, you can avoid using a `ATTEMPT ... FINISH` block, and simply use the `PASS` macro.
|
||
|
||
```
|
||
PREPARE_ERROR(e);
|
||
PASS(e, some_method_that_may_fail());
|
||
SUCCEED_RETURN(e);
|
||
```
|
||
|
||
This does the same thing as this, but with less code:
|
||
|
||
```
|
||
PREPARE_ERROR(e);
|
||
ATTEMPT {
|
||
CATCH(e, some_method_that_may_fail());
|
||
} CLEANUP {
|
||
} PROCESS(e) {
|
||
} FINISH(e, true);
|
||
SUCCEED_RETURN(e);
|
||
```
|
||
|
||
# Handling errors
|
||
|
||
Inside of the `PROCESS { ... }` block, you must handle any errors that occurred during the `ATTEMPT { ... }` block. You do this with `HANDLE`, `HANDLE_GROUP`, and `HANDLE_DEFAULT`.
|
||
|
||
## Handling a specific error with HANDLE
|
||
|
||
In order to handle a specific error code, use the `HANDLE` macro.
|
||
|
||
```c
|
||
} PROCESS(errctx) {
|
||
} HANDLE(errctx, AKERR_NULLPOINTER) {
|
||
// Something is complaining about a null pointer error. Do something about it.
|
||
} // ...
|
||
```
|
||
|
||
## Handling a group of errors with HANDLE_GROUP
|
||
|
||
In order to handle a group of related errors that all require the same failure behavior, use `HANDLE` followed by `HANDLE_GROUP`. For example, to handle a scenario where an IO error, key error, and index error all need to be handled the same way:
|
||
|
||
```c
|
||
} PROCESS(errctx) {
|
||
} HANDLE(errctx, AKERR_IO) {
|
||
} HANDLE_GROUP(errctx, AKERR_KEY) {
|
||
} HANDLE_GROUP(errctx, AKERR_INDEX) {
|
||
// error handling code goes here
|
||
}
|
||
```
|
||
|
||
This creates a fallthrough mechanism where all 3 errors get the same error handling code. Note that while the cases fall through, you can still (if desired) put some code specific to each error in that error's `HANDLE` or `HANDLE_GROUP` block; but this is not required, only the final handler needs to get any code.
|
||
|
||
The fallthrough behavior stops as soon as another `HANDLE` macro is encountered. For example, in this example, `AKERR_IO`, `AKERR_KEY` and `AKERR_INDEX` are all handled as a group, but `AKERR_RELATIONSHIP` is not.
|
||
|
||
```c
|
||
} PROCESS(errctx) {
|
||
} HANDLE(errctx, AKERR_IO) {
|
||
} HANDLE_GROUP(errctx, AKERR_KEY) {
|
||
} HANDLE_GROUP(errctx, AKERR_INDEX) {
|
||
// This code handles 3 error cases
|
||
} HANDLE(errctx, AKERR_RELATIONSHIP) {
|
||
// This code handles 1 error case
|
||
}
|
||
```
|
||
|
||
# Returning success or failure from functions returning akerr_ErrorContext *
|
||
|
||
If at all possible, when using this library, your functiions should return `akerr_ErrorContext *`. When returning from such functions, you should use the `SUCCEED_RETURN` and `FAIL_RETURN` macros.
|
||
|
||
## SUCCEED_RETURN
|
||
|
||
This macro is used when your function has reached the end of its happy code path and is prepared to exit successfully. This sets the akerr_ErrorContext to a successful state and exits the function.
|
||
|
||
```c
|
||
PREPARE_ERROR(errctx);
|
||
ATTEMPT {
|
||
// ... stuff
|
||
} CLEANUP {
|
||
} PROCESS(errctx) {
|
||
} FINISH(errctx, true);
|
||
SUCCEED_RETURN(errctx);
|
||
```
|
||
|
||
## FAIL_RETURN
|
||
|
||
If the code path in the current function reaches a state wherein an error must be set and the function must return early, you can use `FAIL_RETURN` to accomplish this. Note that this should not be used inside of an `ATTEMPT { ... }` block; this immediately exits the function, preventing a `CLEANUP { ... }` block from executing. This can be safely used from inside of a `CLEANUP` or `PROCESS` block, or from anywhere within the function not inside of an `ATTEMPT { ... }` block.
|
||
|
||
The function allows you to provide printf-style variable arguments to provide a meaningful failure message.
|
||
|
||
```c
|
||
PREPARE_ERROR(errctx);
|
||
FAIL_RETURN(AKERR_BEHAVIOR, "Something went horribly wrong!")
|
||
```
|
||
|
||
## Conditionally failing and returning
|
||
|
||
In addition to `FAIL_RETURN` you can also test for zero or non-zero conditions, set an error, and return from the function immediately. Use the `FAIL_ZERO_RETURN` and `FAIL_NONZERO_RETURN` macros for this. These macros can be used anywhere that `FAIL_RETURN` can be used.
|
||
|
||
```c
|
||
PREPARE_ERROR(errctx);
|
||
FAIL_ZERO_RETURN(errctx, (somePointer == NULL), AKERR_NULLPOINTER, "Someone gave me a NULL pointer")
|
||
```
|
||
|
||
```c
|
||
PREPARE_ERROR(errctx);
|
||
FAIL_NONZERO_RETURN(errctx, strcmp("not", "equal"), AKERR_VALUE, "Strings are not equal")
|
||
```
|
||
|
||
# Uncaught errors
|
||
|
||
## Misbehaving methods
|
||
|
||
Any function which returns `akerr_ErrorContext *` and completes successfully MUST call `SUCCEED_RETURN(errctx)`. Failure to do this may result in an invalid `akerr_ErrorContext *` being returned, which will cause an `AKERR_BEHAVIOR` error to be triggered from your code.
|
||
|
||
## Ensuring that all error codes are captured
|
||
|
||
Any function which returns `akerr_ErrorContext *` should also be marked with `AKERROR_NOIGNORE`.
|
||
|
||
```c
|
||
akerr_ErrorContext AKERROR_NOIGNORE *f(...);
|
||
```
|
||
|
||
This will cause a compile-time error if the return value of such a function is not used. "Used" here means assigned to a variable - it does not necessarily mean that the value is checked. However assuming that such functions are called inside of `ATTEMPT { ... }` blocks, it is safe to assume that such returns will be caught with `CATCH(...)`; therefore this error is a generally effective safeguard against careless coding where errors are not checked.
|
||
|
||
Beware that `AKERROR_NOIGNORE` is not a failsafe - it implements the `warn_unused_result` mechanic. By design users may explicitly ignore an error code from a function marked with `warn_unused_result` by explicitly casting the return to `void`.
|
||
|
||
```c
|
||
#define AKERROR_NOIGNORE __attribute__((warn_unused_result))
|
||
```
|
||
|
||
## Stack Traces
|
||
|
||
Whenever an error is captured using the `FAIL_*` or `CATCH` methods, and is unhandled such that it manages to propagate all the way to the top of the caller stack without being managed, the last `FINISH` macro to touch the error will trigger a stack trace and kill the program.
|
||
|
||
Consider the `tests/err_trace.c` program which intentionally triggers this behavior. It produces output like this:
|
||
|
||
```
|
||
tests/err_trace.c:func2:7: 1 (Null Pointer Error) : This is a failure in func2
|
||
tests/err_trace.c:func2:10
|
||
tests/err_trace.c:func1:18: Detected error 0 from array (refcount 1)
|
||
tests/err_trace.c:func1:18
|
||
tests/err_trace.c:func1:21
|
||
tests/err_trace.c:main:30: Detected error 0 from array (refcount 1)
|
||
tests/err_trace.c:main:30
|
||
tests/err_trace.c:main:33: Unhandled Error 1 (Null Pointer Error): This is a failure in func2
|
||
```
|
||
|
||
From bottom to top, we have:
|
||
|
||
* The last line printed is the `FINISH` macro call that triggered the stacktrace.
|
||
* Above that, the `CATCH()` inside of `main()` which caught the exception from `func1()` but did not handle it
|
||
* Above that, a statement that the error was detected in the `CATCH()` statement at the same line
|
||
* Above that, the `FINISH()` macro in the `func1` method which detected the presence of an unhandled error and returned it up the calling stack
|
||
* Above that, the `CATCH()` macro in the `func1` method which caught the error coming out of `func2()`
|
||
* Above that, a statement that the error was detected in the `CATCH()` statement at the same line
|
||
* Above that, the `FINISH()` macro in `func2()` which detected an unhandled error and passed it out of the function
|
||
* Above that, a reference to the line where the `FAIL()` macro set the error code and provided the message which is printed here
|
||
|