The README was 794 lines: the summary, the design rationale, the whole macro reference, the threading contract, the build internals and the exit-status specification in one file. It is now 178 lines -- summary, installation, quickstart, and an index -- and the reference material lives in docs/, one file per topic: architecture, usage, status-codes, uncaught-errors, exit-status, thread-safety, building. The prose moved as written. Inbound references followed it: UPGRADING.md, TODO.md, include/akerror.tmpl.h and tests/err_threads_handoff.c now name the docs/ file that owns the text they cite, and AGENTS.md says where new documentation goes so the README does not grow back. Five factual errors fixed in the moved text: - Both NULL-pointer examples inverted their test. FAIL_ZERO_* fails when the expression is zero, so `(somePointer == NULL)` failed on a *valid* pointer. They now read `(somePointer != NULL)`. - AKERROR_NOIGNORE, four times including the #define, is AKERR_NOIGNORE. - FINISH_NORExbTURN is FINISH_NORETURN. - "functiions" is "functions". - The architecture link pointed at include/akerror.h, which is generated and not in the tree; it points at include/akerror.tmpl.h. The quickstart is new text. It compiles under -Wall -Wextra -Werror and was run through all three of its paths: handled usage error exits 0, unhandled IO error prints a trace and exits with the status, success exits 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
3.2 KiB
Exit status
Never call exit() with an akerr status. Call akerr_exit().
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:
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.