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>
77 lines
3.2 KiB
Markdown
77 lines
3.2 KiB
Markdown
# 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.
|