2025-07-21 07:19:41 -04:00
# Summary
2025-07-20 21:44:17 -04:00
2025-07-21 07:19:41 -04:00
This library provides a TRY/CATCH style exception handling mechanism for C.
2026-06-27 08:42:08 -04:00

Make the error pool and status registry thread safe
Every entry point may now be called from any thread. akerr_init() runs
exactly once however many threads race into it, the pool hands each slot
to exactly one thread, and reservations, registrations and lookups are
serialized against each other.
One recursive lock covers both tables (src/lock.h, private). Recursive
because raising an error re-enters the library -- FAIL needs a pool slot
and a status name -- and single because two locks would mean an ordering
to get wrong. Registry bodies that use the early-returning FAIL_*_RETURN
macros are split into *_locked functions behind wrappers that take and
release the lock on one path; consumer callbacks are never called under
it.
This is an ABI break, hence 2.0.0 and SOVERSION 2:
- akerr_next_error() now returns a context that already holds its
reference. Finding a free slot and claiming it has to be one operation,
or two threads scanning at once are handed the same slot.
ENSURE_ERROR_READY no longer increments.
- __akerr_last_ignored is thread-local, as is the last-ditch context used
to report akerr_release_error(NULL).
The threading backend is chosen at configure time by AKERR_THREADS
(auto, pthread, none). auto fails the configure when it cannot find
POSIX threads rather than quietly building a library that reports itself
thread safe and is not. generrno.sh stamps the decision into the
generated header as AKERR_THREAD_SAFE, so a consumer cannot disagree
with the library about it.
Tests: err_threads_init, err_threads_pool and err_threads_registry
assert exclusive slot ownership, exactly one winner for a contested
range, and every registered name readable back under contention.
AKERR_SANITIZE builds the library and the tests with any sanitizer;
scripts/thread_test.sh runs the suite under ThreadSanitizer and CI runs
it. Removing the pool lock makes both the sanitizer and the plain
assertions fail, so the tests are not vacuous.
Documented in README.md and UPGRADING.md, including what this does not
cover: renaming a status while another thread looks it up, and which of
two simultaneous unhandled errors sets the exit status.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:31:22 -04:00
## Upgrading
2026-07-30 13:53:39 -04:00
Stop an unhandled error from exiting zero
An unhandled error could kill the process and still report success. The
default handler ended in exit(errctx->status), and an exit status is one
byte wide: the kernel keeps the low 8 bits of the argument and discards
the rest. Consumer statuses start at AKERR_FIRST_CONSUMER_STATUS (256),
so the first status any consumer can reserve exited 0 and a shell saw a
clean run. Status 300 exited 44, an unrelated error's code.
There is no wider exit() to reach for. _exit(), _Exit(), quick_exit()
and the raw exit_group syscall all truncate identically, and even
waitid(), whose si_status is a full int, reports the truncated value --
the truncation happened before the parent looked.
akerr_exit() now owns that mapping and the default handler calls it: 0
exits 0, 1 through 255 exit the status, and anything else exits
AKERR_EXIT_STATUS_UNREPRESENTABLE (125) rather than a low byte that is
either a lie or a claim of success. Only values that were already being
delivered wrong behave differently. Call it instead of exit() anywhere
you leave the process on a status; it is declared AKERR_NORETURN.
akerr_exit(0) exits 0, because 0 is this library's success status. That
is not a hole in the rule: PROCESS opens with case 0, which marks a zero
status handled, so a successful context never reaches FINISH_NORETURN's
call to the handler at all.
tests/err_exit_status.c drives one table through akerr_exit() and
through the default handler in forked children and requires identical
exit codes, so the handler cannot grow a mapping of its own. With the
clamp removed it fails with "akerr_exit(256) exited 0, want 125". The
full-width status was already reaching the log and still does, which the
same test asserts against the captured stack trace.
2.0.1. No ABI break: the soname stays libakerror.so.2 and nothing that
already existed changed shape. akerr_exit() is a new exported symbol, so
a consumer that starts calling it needs 2.0.1 at link time.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 07:25:37 -04:00
2.0.1 fixes an unhandled error killing the process and still reporting success:
the exit code was the status truncated to a byte, and every consumer status
starts at 256. Use `akerr_exit()` instead of `exit()` — see
[Exit status ](#exit-status ). No ABI break.
Make the error pool and status registry thread safe
Every entry point may now be called from any thread. akerr_init() runs
exactly once however many threads race into it, the pool hands each slot
to exactly one thread, and reservations, registrations and lookups are
serialized against each other.
One recursive lock covers both tables (src/lock.h, private). Recursive
because raising an error re-enters the library -- FAIL needs a pool slot
and a status name -- and single because two locks would mean an ordering
to get wrong. Registry bodies that use the early-returning FAIL_*_RETURN
macros are split into *_locked functions behind wrappers that take and
release the lock on one path; consumer callbacks are never called under
it.
This is an ABI break, hence 2.0.0 and SOVERSION 2:
- akerr_next_error() now returns a context that already holds its
reference. Finding a free slot and claiming it has to be one operation,
or two threads scanning at once are handed the same slot.
ENSURE_ERROR_READY no longer increments.
- __akerr_last_ignored is thread-local, as is the last-ditch context used
to report akerr_release_error(NULL).
The threading backend is chosen at configure time by AKERR_THREADS
(auto, pthread, none). auto fails the configure when it cannot find
POSIX threads rather than quietly building a library that reports itself
thread safe and is not. generrno.sh stamps the decision into the
generated header as AKERR_THREAD_SAFE, so a consumer cannot disagree
with the library about it.
Tests: err_threads_init, err_threads_pool and err_threads_registry
assert exclusive slot ownership, exactly one winner for a contested
range, and every registered name readable back under contention.
AKERR_SANITIZE builds the library and the tests with any sanitizer;
scripts/thread_test.sh runs the suite under ThreadSanitizer and CI runs
it. Removing the pool lock makes both the sanitizer and the plain
assertions fail, so the tests are not vacuous.
Documented in README.md and UPGRADING.md, including what this does not
cover: renaming a status while another thread looks it up, and which of
two simultaneous unhandled errors sets the exit status.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:31:22 -04:00
2.0.0 makes the library thread safe. That is an ABI break — `__akerr_last_ignored`
became thread-local storage and the pool now takes its own reference — so
everything built against a 1.x header must be rebuilt. 1.0.0 replaced the
consumer-sized status-name array with a private, ownership-enforced registry.
Stop an unhandled error from exiting zero
An unhandled error could kill the process and still report success. The
default handler ended in exit(errctx->status), and an exit status is one
byte wide: the kernel keeps the low 8 bits of the argument and discards
the rest. Consumer statuses start at AKERR_FIRST_CONSUMER_STATUS (256),
so the first status any consumer can reserve exited 0 and a shell saw a
clean run. Status 300 exited 44, an unrelated error's code.
There is no wider exit() to reach for. _exit(), _Exit(), quick_exit()
and the raw exit_group syscall all truncate identically, and even
waitid(), whose si_status is a full int, reports the truncated value --
the truncation happened before the parent looked.
akerr_exit() now owns that mapping and the default handler calls it: 0
exits 0, 1 through 255 exit the status, and anything else exits
AKERR_EXIT_STATUS_UNREPRESENTABLE (125) rather than a low byte that is
either a lie or a claim of success. Only values that were already being
delivered wrong behave differently. Call it instead of exit() anywhere
you leave the process on a status; it is declared AKERR_NORETURN.
akerr_exit(0) exits 0, because 0 is this library's success status. That
is not a hole in the rule: PROCESS opens with case 0, which marks a zero
status handled, so a successful context never reaches FINISH_NORETURN's
call to the handler at all.
tests/err_exit_status.c drives one table through akerr_exit() and
through the default handler in forked children and requires identical
exit codes, so the handler cannot grow a mapping of its own. With the
clamp removed it fails with "akerr_exit(256) exited 0, want 125". The
full-width status was already reaching the log and still does, which the
same test asserts against the captured stack trace.
2.0.1. No ABI break: the soname stays libakerror.so.2 and nothing that
already existed changed shape. akerr_exit() is a new exported symbol, so
a consumer that starts calling it needs 2.0.1 at link time.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 07:25:37 -04:00
See [UPGRADING.md ](UPGRADING.md ) for all three, what was removed, how to migrate,
the capacity limits and how to raise them, and the thread-safety rules.
Enforce status-code ownership and harden the name registry
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>
2026-07-30 18:19:25 -04:00
2026-07-30 13:53:39 -04:00
2026-01-04 22:56:31 -05:00
# Why?
2025-07-21 07:19:41 -04:00
2026-01-04 22:56:31 -05:00
There is nothing wrong with C as it is. This library does not claim to fix some problem with C.
2025-07-21 07:19:41 -04:00
2026-01-04 22:56:31 -05:00
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.
2025-07-21 07:19:41 -04:00
2026-01-04 22:56:31 -05:00
Why? Because some programmers prefer to have the power of C with just a little bit of help in managing their errors.
# Library Architecture
2025-07-20 22:02:21 -04:00
2026-01-04 22:56:31 -05:00
## Philosophy of Use
2025-07-21 07:19:41 -04:00
2025-08-02 14:28:54 -04:00
This library has 6 guiding principles:
2025-07-21 07:19:41 -04:00
2025-08-02 14:28:54 -04:00
* Manually checking every possible return code for every possible meaning of that return code is tedious and prone to miss unpredicted failure cases
2025-08-03 10:13:27 -04:00
* Functions should return rich descriptive error contexts, not values
2025-08-02 14:28:54 -04:00
* 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
2025-07-21 07:19:41 -04:00
2026-01-04 22:56:31 -05:00
## Lifecycle of an error in the AKError library
2026-01-10 10:20:35 -05:00
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
2026-01-10 22:03:14 -05:00
1. At the point where an error occurs, an `akerr_ErrorContext` object is initialized and populated with information regarding the failure
2026-01-10 10:20:35 -05:00
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
2026-01-04 22:56:31 -05:00
7. Steps 2-6 are repeated through as many control structures as are necessary to reach the first level of the control structure
2026-01-10 10:20:35 -05:00
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
2026-01-04 22:56:31 -05:00
## 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
2026-01-10 10:20:35 -05:00
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.
2026-01-04 22:56:31 -05:00
## Functions and Return Codes
2026-01-10 10:20:35 -05:00
This library can catch errors from any function or expression that returns an integer value, or from functions that return `akerr_ErrorContext *` .
2026-01-04 22:56:31 -05:00
2026-01-10 10:20:35 -05:00
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()` .
2026-01-04 22:56:31 -05:00
## Error codes
2026-01-10 10:20:35 -05:00
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.
2026-01-04 22:56:31 -05:00
Enforce status-code ownership and harden the name registry
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>
2026-07-30 18:19:25 -04:00
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
Stop an unhandled error from exiting zero
An unhandled error could kill the process and still report success. The
default handler ended in exit(errctx->status), and an exit status is one
byte wide: the kernel keeps the low 8 bits of the argument and discards
the rest. Consumer statuses start at AKERR_FIRST_CONSUMER_STATUS (256),
so the first status any consumer can reserve exited 0 and a shell saw a
clean run. Status 300 exited 44, an unrelated error's code.
There is no wider exit() to reach for. _exit(), _Exit(), quick_exit()
and the raw exit_group syscall all truncate identically, and even
waitid(), whose si_status is a full int, reports the truncated value --
the truncation happened before the parent looked.
akerr_exit() now owns that mapping and the default handler calls it: 0
exits 0, 1 through 255 exit the status, and anything else exits
AKERR_EXIT_STATUS_UNREPRESENTABLE (125) rather than a low byte that is
either a lie or a claim of success. Only values that were already being
delivered wrong behave differently. Call it instead of exit() anywhere
you leave the process on a status; it is declared AKERR_NORETURN.
akerr_exit(0) exits 0, because 0 is this library's success status. That
is not a hole in the rule: PROCESS opens with case 0, which marks a zero
status handled, so a successful context never reaches FINISH_NORETURN's
call to the handler at all.
tests/err_exit_status.c drives one table through akerr_exit() and
through the default handler in forked children and requires identical
exit codes, so the handler cannot grow a mapping of its own. With the
clamp removed it fails with "akerr_exit(256) exited 0, want 125". The
full-width status was already reaching the log and still does, which the
same test asserts against the captured stack trace.
2.0.1. No ABI break: the soname stays libakerror.so.2 and nothing that
already existed changed shape. akerr_exit() is a new exported symbol, so
a consumer that starts calling it needs 2.0.1 at link time.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 07:25:37 -04:00
definition is needed to use large values. Note that no consumer status can be a
process exit code — see [Exit status ](#exit-status ).
2026-07-30 13:53:39 -04:00
Enforce status-code ownership and harden the name registry
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>
2026-07-30 18:19:25 -04:00
Every library that may coexist in one process must reserve its range during
Raise errors from the status registry instead of returning codes
akerr_reserve_status_range() and akerr_register_status_name() returned
private int enumerations, which was the one place in the library where a
failure was not an akerr_ErrorContext *. They now return one like
everything else: NULL on success, and on refusal an error whose status is
a real code in the library's reserved band, so it can be CATCH-ed,
HANDLE-d, PASS-ed, or left to propagate into a stack trace. Both are
marked AKERR_NOIGNORE, so discarding the result warns at compile time.
AKERR_STATUS_RANGE_OK and AKERR_STATUS_NAME_OK are gone; the remaining
seven codes move into the AKERR_* offset span and get registered names.
AKERR_LAST_LIBRARY_STATUS replaces AKERR_BADEXC as the top of that span
in the reserved-band static assert and the exhaustiveness sweep.
The refusal detail that used to go straight to akerr_log_method now
travels in the error message, so a caller that handles the error decides
whether it is reported. The two-argument akerr_name_for_status() set path
is the exception: it returns a name and cannot raise, so it logs and
releases. akerr_init() likewise has no caller to raise into, so failing
to reserve its own band or name its own codes is logged and fatal --
that can only happen on a misconfigured build, and continuing would
degrade every later stack trace to "Unknown Error".
Move the 1.0.0 upgrade notice out of README.md into UPGRADING.md and
rewrite its return-code tables in terms of the statuses now raised.
Tests: ctest 29/29, coverage 97.5% line / 64.5% branch, mutation 77.5%
(was 77.6%; the new survivors are the fatal init path, which needs a
library built with an undersized name table -- TODO item 7).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 20:58:47 -04:00
initialization. `akerr_reserve_status_range()` and
`akerr_register_status_name()` report failure the way everything else in this
library does — they return `akerr_ErrorContext *` , and they are marked
`AKERR_NOIGNORE` — so a collision is an exception you can `CATCH` , `HANDLE` , or
`PASS` up out of your initialization:
2026-07-30 13:53:39 -04:00
```c
Enforce status-code ownership and harden the name registry
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>
2026-07-30 18:19:25 -04:00
#define MYLIB_OWNER "my-library"
Raise errors from the status registry instead of returning codes
akerr_reserve_status_range() and akerr_register_status_name() returned
private int enumerations, which was the one place in the library where a
failure was not an akerr_ErrorContext *. They now return one like
everything else: NULL on success, and on refusal an error whose status is
a real code in the library's reserved band, so it can be CATCH-ed,
HANDLE-d, PASS-ed, or left to propagate into a stack trace. Both are
marked AKERR_NOIGNORE, so discarding the result warns at compile time.
AKERR_STATUS_RANGE_OK and AKERR_STATUS_NAME_OK are gone; the remaining
seven codes move into the AKERR_* offset span and get registered names.
AKERR_LAST_LIBRARY_STATUS replaces AKERR_BADEXC as the top of that span
in the reserved-band static assert and the exhaustiveness sweep.
The refusal detail that used to go straight to akerr_log_method now
travels in the error message, so a caller that handles the error decides
whether it is reported. The two-argument akerr_name_for_status() set path
is the exception: it returns a name and cannot raise, so it logs and
releases. akerr_init() likewise has no caller to raise into, so failing
to reserve its own band or name its own codes is logged and fatal --
that can only happen on a misconfigured build, and continuing would
degrade every later stack trace to "Unknown Error".
Move the 1.0.0 upgrade notice out of README.md into UPGRADING.md and
rewrite its return-code tables in terms of the statuses now raised.
Tests: ctest 29/29, coverage 97.5% line / 64.5% branch, mutation 77.5%
(was 77.6%; the new survivors are the fatal init path, which needs a
library built with an undersized name table -- TODO item 7).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 20:58:47 -04:00
akerr_ErrorContext AKERR_NOIGNORE *mylib_init(void)
{
PREPARE_ERROR(errctx);
/* Another component owning part of the range propagates to our caller. */
PASS(errctx, akerr_reserve_status_range(256, 16, MYLIB_OWNER));
/* Then name each code, quoting the owner you reserved with. */
PASS(errctx, akerr_register_status_name(MYLIB_OWNER, 256,
"Some Error Code Description"));
SUCCEED_RETURN(errctx);
2026-07-30 13:53:39 -04:00
}
```
Enforce status-code ownership and harden the name registry
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>
2026-07-30 18:19:25 -04:00
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.
2026-01-12 08:33:31 -05:00
Raise errors from the status registry instead of returning codes
akerr_reserve_status_range() and akerr_register_status_name() returned
private int enumerations, which was the one place in the library where a
failure was not an akerr_ErrorContext *. They now return one like
everything else: NULL on success, and on refusal an error whose status is
a real code in the library's reserved band, so it can be CATCH-ed,
HANDLE-d, PASS-ed, or left to propagate into a stack trace. Both are
marked AKERR_NOIGNORE, so discarding the result warns at compile time.
AKERR_STATUS_RANGE_OK and AKERR_STATUS_NAME_OK are gone; the remaining
seven codes move into the AKERR_* offset span and get registered names.
AKERR_LAST_LIBRARY_STATUS replaces AKERR_BADEXC as the top of that span
in the reserved-band static assert and the exhaustiveness sweep.
The refusal detail that used to go straight to akerr_log_method now
travels in the error message, so a caller that handles the error decides
whether it is reported. The two-argument akerr_name_for_status() set path
is the exception: it returns a name and cannot raise, so it logs and
releases. akerr_init() likewise has no caller to raise into, so failing
to reserve its own band or name its own codes is logged and fatal --
that can only happen on a misconfigured build, and continuing would
degrade every later stack trace to "Unknown Error".
Move the 1.0.0 upgrade notice out of README.md into UPGRADING.md and
rewrite its return-code tables in terms of the statuses now raised.
Tests: ctest 29/29, coverage 97.5% line / 64.5% branch, mutation 77.5%
(was 77.6%; the new survivors are the fatal init path, which needs a
library built with an undersized name table -- TODO item 7).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 20:58:47 -04:00
Naming a status outside your reservation raises `AKERR_STATUS_NAME_FOREIGN` , and
naming one nobody reserved raises `AKERR_STATUS_NAME_UNRESERVED` ; a colliding
reservation raises `AKERR_STATUS_RANGE_OVERLAP` , with a message naming the real
owner. See [UPGRADING.md ](UPGRADING.md ) for the full list of statuses these two
functions raise, the capacity limits and how to raise them, and thread-safety
rules.
Enforce status-code ownership and harden the name registry
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>
2026-07-30 18:19:25 -04:00
2026-01-04 22:56:31 -05:00
Make the error pool and status registry thread safe
Every entry point may now be called from any thread. akerr_init() runs
exactly once however many threads race into it, the pool hands each slot
to exactly one thread, and reservations, registrations and lookups are
serialized against each other.
One recursive lock covers both tables (src/lock.h, private). Recursive
because raising an error re-enters the library -- FAIL needs a pool slot
and a status name -- and single because two locks would mean an ordering
to get wrong. Registry bodies that use the early-returning FAIL_*_RETURN
macros are split into *_locked functions behind wrappers that take and
release the lock on one path; consumer callbacks are never called under
it.
This is an ABI break, hence 2.0.0 and SOVERSION 2:
- akerr_next_error() now returns a context that already holds its
reference. Finding a free slot and claiming it has to be one operation,
or two threads scanning at once are handed the same slot.
ENSURE_ERROR_READY no longer increments.
- __akerr_last_ignored is thread-local, as is the last-ditch context used
to report akerr_release_error(NULL).
The threading backend is chosen at configure time by AKERR_THREADS
(auto, pthread, none). auto fails the configure when it cannot find
POSIX threads rather than quietly building a library that reports itself
thread safe and is not. generrno.sh stamps the decision into the
generated header as AKERR_THREAD_SAFE, so a consumer cannot disagree
with the library about it.
Tests: err_threads_init, err_threads_pool and err_threads_registry
assert exclusive slot ownership, exactly one winner for a contested
range, and every registered name readable back under contention.
AKERR_SANITIZE builds the library and the tests with any sanitizer;
scripts/thread_test.sh runs the suite under ThreadSanitizer and CI runs
it. Removing the pool lock makes both the sanitizer and the plain
assertions fail, so the tests are not vacuous.
Documented in README.md and UPGRADING.md, including what this does not
cover: renaming a status while another thread looks it up, and which of
two simultaneous unhandled errors sets the exit status.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:31:22 -04:00
# Thread safety
The library is thread safe as built by default. Every entry point may be called
from any thread at any time, including the first one: `akerr_init()` runs
exactly once no matter how many threads race into it.
What that covers:
* **The error pool.** Finding a free slot in `AKERR_ARRAY_ERROR` and taking its
reference is one operation under a lock, so two threads can never be handed
the same context. A context is then owned by the thread that raised it, all
the way through `CATCH` , `HANDLE` , and release.
* **The status registry.** Reservations and name registrations are serialized
against each other and against lookups. Two threads reserving the same range
cannot both win — exactly one gets `NULL` and the other gets
`AKERR_STATUS_RANGE_OVERLAP` naming the winner.
* **Per-thread state.** The context behind `IGNORE` (`__akerr_last_ignored` ) and
the last-ditch context used to report `akerr_release_error(NULL)` are
thread-local, so one thread's ignored error is never another's.
What it does not cover, and cannot:
* **Sharing one error context between threads.** The library hands a context to
one thread; passing it to another is your synchronization to do.
* **`akerr_log_method` and `akerr_handler_unhandled_error` .** Set them during
startup, before you spawn threads. They are read on every error and the
library never writes them after initialization, so setting one while other
threads are raising errors is a race the library cannot mediate.
* **Renaming a status that other threads are looking up.**
`akerr_name_for_status(status, NULL)` returns a pointer into the registry,
valid for the life of the process; registering a * second * name for the same
status overwrites that buffer in place. Register names during initialization.
Registering a * new * status concurrently is fine.
* **Which unhandled error terminates the process.** An error that reaches
`FINISH_NORETURN` unhandled prints its stack trace and calls
Stop an unhandled error from exiting zero
An unhandled error could kill the process and still report success. The
default handler ended in exit(errctx->status), and an exit status is one
byte wide: the kernel keeps the low 8 bits of the argument and discards
the rest. Consumer statuses start at AKERR_FIRST_CONSUMER_STATUS (256),
so the first status any consumer can reserve exited 0 and a shell saw a
clean run. Status 300 exited 44, an unrelated error's code.
There is no wider exit() to reach for. _exit(), _Exit(), quick_exit()
and the raw exit_group syscall all truncate identically, and even
waitid(), whose si_status is a full int, reports the truncated value --
the truncation happened before the parent looked.
akerr_exit() now owns that mapping and the default handler calls it: 0
exits 0, 1 through 255 exit the status, and anything else exits
AKERR_EXIT_STATUS_UNREPRESENTABLE (125) rather than a low byte that is
either a lie or a claim of success. Only values that were already being
delivered wrong behave differently. Call it instead of exit() anywhere
you leave the process on a status; it is declared AKERR_NORETURN.
akerr_exit(0) exits 0, because 0 is this library's success status. That
is not a hole in the rule: PROCESS opens with case 0, which marks a zero
status handled, so a successful context never reaches FINISH_NORETURN's
call to the handler at all.
tests/err_exit_status.c drives one table through akerr_exit() and
through the default handler in forked children and requires identical
exit codes, so the handler cannot grow a mapping of its own. With the
clamp removed it fails with "akerr_exit(256) exited 0, want 125". The
full-width status was already reaching the log and still does, which the
same test asserts against the captured stack trace.
2.0.1. No ABI break: the soname stays libakerror.so.2 and nothing that
already existed changed shape. akerr_exit() is a new exported symbol, so
a consumer that starts calling it needs 2.0.1 at link time.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 07:25:37 -04:00
`akerr_handler_unhandled_error` , which by default calls `akerr_exit()` . Each
Make the error pool and status registry thread safe
Every entry point may now be called from any thread. akerr_init() runs
exactly once however many threads race into it, the pool hands each slot
to exactly one thread, and reservations, registrations and lookups are
serialized against each other.
One recursive lock covers both tables (src/lock.h, private). Recursive
because raising an error re-enters the library -- FAIL needs a pool slot
and a status name -- and single because two locks would mean an ordering
to get wrong. Registry bodies that use the early-returning FAIL_*_RETURN
macros are split into *_locked functions behind wrappers that take and
release the lock on one path; consumer callbacks are never called under
it.
This is an ABI break, hence 2.0.0 and SOVERSION 2:
- akerr_next_error() now returns a context that already holds its
reference. Finding a free slot and claiming it has to be one operation,
or two threads scanning at once are handed the same slot.
ENSURE_ERROR_READY no longer increments.
- __akerr_last_ignored is thread-local, as is the last-ditch context used
to report akerr_release_error(NULL).
The threading backend is chosen at configure time by AKERR_THREADS
(auto, pthread, none). auto fails the configure when it cannot find
POSIX threads rather than quietly building a library that reports itself
thread safe and is not. generrno.sh stamps the decision into the
generated header as AKERR_THREAD_SAFE, so a consumer cannot disagree
with the library about it.
Tests: err_threads_init, err_threads_pool and err_threads_registry
assert exclusive slot ownership, exactly one winner for a contested
range, and every registered name readable back under contention.
AKERR_SANITIZE builds the library and the tests with any sanitizer;
scripts/thread_test.sh runs the suite under ThreadSanitizer and CI runs
it. Removing the pool lock makes both the sanitizer and the plain
assertions fail, so the tests are not vacuous.
Documented in README.md and UPGRADING.md, including what this does not
cover: renaming a status while another thread looks it up, and which of
two simultaneous unhandled errors sets the exit status.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:31:22 -04:00
thread's trace is whole — the buffer belongs to its context, and each line is
one call to `akerr_log_method` — but if two threads get there at the same
instant, both traces print and the exit status is whichever one won.
There is one lock, it is recursive, and it covers both the pool and the
registry. That means error construction is serialized across threads: raising an
error is the exceptional path, and correctness there is worth more than
throughput. A program that raises errors on its hot path will feel it.
`AKERR_THREAD_SAFE` in the generated header is `1` for a thread-safe build, so a
consumer can check what it linked against:
```c
#if AKERR_THREAD_SAFE
/* ... start worker threads ... */
#endif
```
## Building single threaded
The threading backend is chosen when libakerror is configured. `auto` (the
default) takes POSIX threads, and **fails the configure ** if it cannot find
them rather than quietly producing a library that says it is thread safe and is
not. To mean it:
```sh
cmake -S . -B build -DAKERR_THREADS=none
```
That builds with no locking and no thread-local storage, stamps
`AKERR_THREAD_SAFE 0` into the header, and calling the library from more than
one thread is then undefined.
## Proving it
The thread tests (`tests/err_threads_*.c` ) assert the properties above directly:
exclusive ownership of pool slots, exactly one winner for a contested range,
every registered name readable back. They run in the normal suite. The run that
proves the * absence * of a data race underneath them is ThreadSanitizer:
```sh
scripts/thread_test.sh
```
which configures `build/tsan` with `-DAKERR_SANITIZE=thread` , builds the library
and every test with it, and runs the suite. Under that build a sanitizer report
fails the test rather than being printed and passed over.
2026-01-04 22:56:31 -05:00
# Installation
```bash
cmake -S . -B build
cmake --build build
cmake --install build
```
2026-01-12 08:33:31 -05:00
## 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` .
2026-01-04 22:56:31 -05:00
## Dependencies
Make the error pool and status registry thread safe
Every entry point may now be called from any thread. akerr_init() runs
exactly once however many threads race into it, the pool hands each slot
to exactly one thread, and reservations, registrations and lookups are
serialized against each other.
One recursive lock covers both tables (src/lock.h, private). Recursive
because raising an error re-enters the library -- FAIL needs a pool slot
and a status name -- and single because two locks would mean an ordering
to get wrong. Registry bodies that use the early-returning FAIL_*_RETURN
macros are split into *_locked functions behind wrappers that take and
release the lock on one path; consumer callbacks are never called under
it.
This is an ABI break, hence 2.0.0 and SOVERSION 2:
- akerr_next_error() now returns a context that already holds its
reference. Finding a free slot and claiming it has to be one operation,
or two threads scanning at once are handed the same slot.
ENSURE_ERROR_READY no longer increments.
- __akerr_last_ignored is thread-local, as is the last-ditch context used
to report akerr_release_error(NULL).
The threading backend is chosen at configure time by AKERR_THREADS
(auto, pthread, none). auto fails the configure when it cannot find
POSIX threads rather than quietly building a library that reports itself
thread safe and is not. generrno.sh stamps the decision into the
generated header as AKERR_THREAD_SAFE, so a consumer cannot disagree
with the library about it.
Tests: err_threads_init, err_threads_pool and err_threads_registry
assert exclusive slot ownership, exactly one winner for a contested
range, and every registered name readable back under contention.
AKERR_SANITIZE builds the library and the tests with any sanitizer;
scripts/thread_test.sh runs the suite under ThreadSanitizer and CI runs
it. Removing the pool lock makes both the sanitizer and the plain
assertions fail, so the tests are not vacuous.
Documented in README.md and UPGRADING.md, including what this does not
cover: renaming a status while another thread looks it up, and which of
two simultaneous unhandled errors sets the exit status.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:31:22 -04:00
This library depends upon `stdlib` , and upon POSIX threads unless it is built
with `-DAKERR_THREADS=none` (see "Thread safety" above). 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:
2026-01-04 22:56:31 -05:00
- `memset` function
- `strncpy` function
Enforce status-code ownership and harden the name registry
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>
2026-07-30 18:19:25 -04:00
- `strlen` function
- `strcmp` function
2026-01-04 22:56:31 -05:00
- `sprintf` function
- `exit` function
- `bool` type
- `NULL` type
Enforce status-code ownership and harden the name registry
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>
2026-07-30 18:19:25 -04:00
- `INT_MAX` constant
- `PATH_MAX` constant
2026-01-04 22:56:31 -05:00
... then you can compile it thusly:
```
2026-01-10 10:20:35 -05:00
cmake -S . -B build -DAKERR_USE_STDLIB=OFF
2026-01-04 22:56:31 -05:00
cmake --build build
cmake --install build
```
2025-08-03 10:13:27 -04:00
# Using the library
2026-01-04 22:56:31 -05:00
## Setting up your project
2025-08-03 10:13:27 -04:00
Include it
```c
2026-01-04 22:56:31 -05:00
#include <akerror.h>
2025-08-03 10:13:27 -04:00
```
2026-01-04 22:56:31 -05:00
Link the library directly, or
2025-08-03 10:13:27 -04:00
```sh
2026-01-04 22:56:31 -05:00
cc -lakerror
2025-08-03 10:13:27 -04:00
```
2026-01-04 22:56:31 -05:00
Using pkg-config, or
2025-08-03 10:13:27 -04:00
```sh
2026-01-04 22:56:31 -05:00
pkg-config akerror --cflags
pkg-config akerror --ldflags
2025-08-03 10:13:27 -04:00
```
Using cmake:
```cmake
2026-01-04 22:56:31 -05:00
find_package(akerror REQUIRED)
pkg_check_modules(akerror REQUIRED akerror)
target_link_libraries(YOUR_TARGET PRIVATE akerror::akerror)
2025-08-03 10:13:27 -04:00
```
2026-05-15 19:41:22 -04:00
Using this project as a submodule with cmake:
```cmake
add_subdirectory(deps/libakerror EXCLUDE_FROM_ALL)
target_link_libraries(YOUR_PROJECT PRIVATE akerror::akerror)
```
2025-07-21 07:19:41 -04:00
2026-01-04 22:56:31 -05:00
## (Optional) Configuring the logging function
2025-07-21 07:19:41 -04:00
2026-01-04 22:56:31 -05:00
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:
2025-07-21 07:19:41 -04:00
2026-01-04 22:56:31 -05:00
```
void my_logger(const char *fmt, ...)
{
/* ... do something */
}
2025-08-02 15:07:08 -04:00
2026-01-04 22:56:31 -05:00
/* set your custom error handler */
2026-01-12 09:26:28 -05:00
akerr_log_method = &my_logger;
2026-01-04 22:56:31 -05:00
/* proceed to use the library */
2025-08-02 15:07:08 -04:00
```
2026-01-04 22:56:31 -05:00
## Setting Up the Error Context
2025-07-21 07:19:41 -04:00
Before you can use any of these macros you must set up an error context inside of the current scope.
```c
PREPARE_ERROR(errctx);
```
2026-01-10 10:20:35 -05:00
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.
2025-07-21 07:19:41 -04:00
2026-01-04 22:56:31 -05:00
## Attempting an Operation
2025-07-20 21:44:17 -04:00
2025-07-21 07:19:41 -04:00
```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.
2026-01-12 08:33:31 -05:00
`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.
2026-01-10 22:03:14 -05:00
2025-07-21 07:19:41 -04:00
# Capturing errors
Inside of an `ATTEMPT` block, any operation which could generate or represent an error should be wrapped in one of several macros.
2026-01-10 10:20:35 -05:00
## Capturing errors from functions which return akerr_ErrorContext *
2025-07-21 07:19:41 -04:00
2026-01-10 10:20:35 -05:00
For functions that return `akerr_ErrorContext *` , you should use the `CATCH` macro.
2025-07-21 07:19:41 -04:00
```c
ATTEMPT {
CATCH(errctx, errorGeneratingFunction())
} // ...
```
2026-01-10 10:20:35 -05:00
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.
2025-07-21 07:19:41 -04:00
2026-07-28 08:49:29 -04:00
(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.)
2025-07-21 07:19:41 -04:00
## 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 {
2026-01-10 10:20:35 -05:00
FAIL_ZERO_BREAK(errctx, (somePointer == NULL), AKERR_NULLPOINTER, "Someone gave me a NULL pointer")
2025-07-21 07:19:41 -04:00
} // ...
```
2025-07-20 21:44:17 -04:00
2025-07-21 07:19:41 -04:00
Here is an example of checking for two strings that are not equal
```c
ATTEMPT {
2026-01-10 10:20:35 -05:00
FAIL_NONZERO_BREAK(errctx, strcmp("not", "equal"), AKERR_VALUE, "Strings are not equal")
2025-07-21 07:19:41 -04:00
} // ...
```
When either of these two macros are used, the `ATTEMPT` block is immediately exited, and the `CLEANUP` block begins.
2026-07-28 08:49:29 -04:00
## 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);
```
2026-05-15 19:41:22 -04:00
# 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);
```
2025-07-21 07:19:41 -04:00
# 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) {
2026-01-10 10:20:35 -05:00
} HANDLE(errctx, AKERR_NULLPOINTER) {
2025-07-21 07:19:41 -04:00
// 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) {
2026-01-10 10:20:35 -05:00
} HANDLE(errctx, AKERR_IO) {
} HANDLE_GROUP(errctx, AKERR_KEY) {
} HANDLE_GROUP(errctx, AKERR_INDEX) {
2025-07-21 07:19:41 -04:00
// 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.
2026-01-10 10:20:35 -05:00
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.
2025-07-21 07:19:41 -04:00
```c
} PROCESS(errctx) {
2026-01-10 10:20:35 -05:00
} HANDLE(errctx, AKERR_IO) {
} HANDLE_GROUP(errctx, AKERR_KEY) {
} HANDLE_GROUP(errctx, AKERR_INDEX) {
2025-07-21 07:19:41 -04:00
// This code handles 3 error cases
2026-01-10 10:20:35 -05:00
} HANDLE(errctx, AKERR_RELATIONSHIP) {
2025-07-21 07:19:41 -04:00
// This code handles 1 error case
}
```
2026-01-10 10:20:35 -05:00
# Returning success or failure from functions returning akerr_ErrorContext *
2025-07-21 07:19:41 -04:00
2026-01-10 10:20:35 -05:00
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.
2025-07-21 07:19:41 -04:00
## SUCCEED_RETURN
2026-01-10 10:20:35 -05:00
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.
2025-07-21 07:19:41 -04:00
```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);
2026-01-10 10:20:35 -05:00
FAIL_RETURN(AKERR_BEHAVIOR, "Something went horribly wrong!")
2025-07-21 07:19:41 -04:00
```
## 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);
2026-01-10 10:20:35 -05:00
FAIL_ZERO_RETURN(errctx, (somePointer == NULL), AKERR_NULLPOINTER, "Someone gave me a NULL pointer")
2025-07-21 07:19:41 -04:00
```
```c
PREPARE_ERROR(errctx);
2026-01-10 10:20:35 -05:00
FAIL_NONZERO_RETURN(errctx, strcmp("not", "equal"), AKERR_VALUE, "Strings are not equal")
2025-07-21 07:19:41 -04:00
```
2025-07-20 21:44:17 -04:00
2025-08-02 14:28:54 -04:00
# Uncaught errors
2026-05-15 19:41:22 -04:00
## 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.
2025-08-02 14:28:54 -04:00
## Ensuring that all error codes are captured
2026-05-15 19:41:22 -04:00
Any function which returns `akerr_ErrorContext *` should also be marked with `AKERROR_NOIGNORE` .
2025-08-02 14:28:54 -04:00
```c
2026-05-15 19:41:22 -04:00
akerr_ErrorContext AKERROR_NOIGNORE *f(...);
2025-08-02 14:28:54 -04:00
```
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.
2026-05-15 19:41:22 -04:00
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` .
2025-08-02 14:32:47 -04:00
```c
2026-05-15 19:41:22 -04:00
#define AKERROR_NOIGNORE __attribute__((warn_unused_result))
2025-08-02 14:32:47 -04:00
```
2025-08-02 14:28:54 -04:00
## Stack Traces
2025-07-21 08:54:26 -04:00
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
2026-01-10 09:50:17 -05:00
tests/err_trace.c:func1:18: Detected error 0 from array (refcount 1)
2025-07-21 08:54:26 -04:00
tests/err_trace.c:func1:18
tests/err_trace.c:func1:21
2026-01-10 09:50:17 -05:00
tests/err_trace.c:main:30: Detected error 0 from array (refcount 1)
2025-07-21 08:54:26 -04:00
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
2026-01-04 22:56:31 -05:00
Stop an unhandled error from exiting zero
An unhandled error could kill the process and still report success. The
default handler ended in exit(errctx->status), and an exit status is one
byte wide: the kernel keeps the low 8 bits of the argument and discards
the rest. Consumer statuses start at AKERR_FIRST_CONSUMER_STATUS (256),
so the first status any consumer can reserve exited 0 and a shell saw a
clean run. Status 300 exited 44, an unrelated error's code.
There is no wider exit() to reach for. _exit(), _Exit(), quick_exit()
and the raw exit_group syscall all truncate identically, and even
waitid(), whose si_status is a full int, reports the truncated value --
the truncation happened before the parent looked.
akerr_exit() now owns that mapping and the default handler calls it: 0
exits 0, 1 through 255 exit the status, and anything else exits
AKERR_EXIT_STATUS_UNREPRESENTABLE (125) rather than a low byte that is
either a lie or a claim of success. Only values that were already being
delivered wrong behave differently. Call it instead of exit() anywhere
you leave the process on a status; it is declared AKERR_NORETURN.
akerr_exit(0) exits 0, because 0 is this library's success status. That
is not a hole in the rule: PROCESS opens with case 0, which marks a zero
status handled, so a successful context never reaches FINISH_NORETURN's
call to the handler at all.
tests/err_exit_status.c drives one table through akerr_exit() and
through the default handler in forked children and requires identical
exit codes, so the handler cannot grow a mapping of its own. With the
clamp removed it fails with "akerr_exit(256) exited 0, want 125". The
full-width status was already reaching the log and still does, which the
same test asserts against the captured stack trace.
2.0.1. No ABI break: the soname stays libakerror.so.2 and nothing that
already existed changed shape. akerr_exit() is a new exported symbol, so
a consumer that starts calling it needs 2.0.1 at link time.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 07:25:37 -04:00
## Exit status
**Never call `exit()` with an akerr status. Call `akerr_exit()` .**
```c
void akerr_exit(int status);
```
This applies everywhere you are leaving the process on account of a status, not
just in an unhandled-error handler: a CLI's top-level `HANDLE` block, an
initialization routine that cannot continue, a `main()` that ends by reporting
the status it finished with. One function owns the mapping so that a given
status produces the same exit code no matter which of your exits it left by.
The mapping exists because a process exit status is one byte wide. `exit()`
accepts an `int` and the kernel keeps the low 8 bits of it — `_exit()` ,
`_Exit()` , `quick_exit()` and the raw `exit_group` syscall all behave
identically, and even `waitid()` , whose `si_status` is a full `int` , sees the
truncated value because the truncation happened before the parent looked. There
is no wider `exit()` to reach for.
That leaves 0 through 255 as the only statuses an exit code can carry, and
consumer statuses start at `AKERR_FIRST_CONSUMER_STATUS` (256) — so * no * consumer
status can be an exit code:
```
status exit code
0 0 (success)
1 .. 255 the status
negative, or > 255 AKERR_EXIT_STATUS_UNREPRESENTABLE (125)
```
Passing the low byte instead would have made status 256 exit 0 and report
success to the shell, and status 300 exit 44 — an unrelated error's code. 125 is
the conventional "the tool itself failed" status; 126, 127 and 128+*n* already
belong to the shell.
Status 0 exits 0, because 0 is this library's success status. That is not a hole
in the rule that an unhandled error never exits 0: `PROCESS` opens with `case
0`, which marks a zero status handled, so a successful context cannot reach
`FINISH_NORETURN` 's call to the handler at all.
Above 255, the exit code tells you the process died of an error, not which one.
125 is inside the library's reserved band and so is also some host's `errno` , and
every status above 255 collapses onto it. **The stack trace is what identifies
the error** — it is printed before the handler runs, carrying the status at full
width along with its registered name.
### Replacing the handler
After the trace is printed, `FINISH_NORETURN` calls
`akerr_handler_unhandled_error` . The default implementation,
`akerr_default_handler_unhandled_error()` , hands `errctx->status` to
`akerr_exit()` (a NULL context exits 1). Replace it if you need something else
to happen first — a core dump, a crash reporter, a flush — and finish by calling
`akerr_exit()` so the exit code still means what it means everywhere else:
```c
static void mylib_handler(akerr_ErrorContext *e)
{
mylib_flush_telemetry();
if ( e == NULL ) {
akerr_exit(AKERR_API);
}
akerr_exit(e->status);
}
akerr_handler_unhandled_error = &mylib_handler;
```
`akerr_exit()` is declared `AKERR_NORETURN` , so the compiler knows a handler
ending in one of those calls is complete rather than falling off the end.
Set the handler once, before you start any threads. `tests/err_custom_handler.c`
installs one that does not exit at all, which is how the test suite asserts on
unhandled errors without dying.