Files
libakerror/README.md

179 lines
7.0 KiB
Markdown
Raw Normal View History

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
![build badge](https://source.starfort.tech/andrew/libakerror/actions/workflows/ci.yaml/badge.svg?branch=main)
# Why?
2025-07-21 07:19:41 -04: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
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
Why? Because some programmers prefer to have the power of C with just a little bit of help in managing their errors.
This library has 6 guiding principles:
2025-07-21 07:19:41 -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
* Functions should return rich descriptive error contexts, not values
* Uncaught errors should cause program termination with a stacktrace
* Dynamic memory allocation is the source of many errors and should be avoided if possible
* Manipulating the call stack directly is error prone and dangerous
* Declaring, capturing, and reacting to errors should be intuitive and no more difficult than managing return codes
2025-07-21 07:19:41 -04:00
Split the README reference material into docs/ 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>
2026-08-01 16:56:07 -04:00
# Documentation
Document and test handing an error context between threads The thread-safety section filed two different things under "does not cover, and cannot": sharing a context between threads, and passing one to another thread. Only the first is unsupported. Transfer already works by construction -- the reference count is the only field the library reads across an ownership boundary, and it is only ever touched under the pool lock, so akerr_release_error() does not care which thread checked the slot out. The pool is process-global, not thread-local, so a context outlives the thread that raised it. Calling that unsupported told readers the worker/collector shape was off the table, which either cost them the pattern or cost them the stack trace when they rolled their own struct instead. Split the bullet: transfer joins the covered list and gets its own section with the rule, the worked pattern, and the four receiving-side hazards (PREPARE_ERROR cannot adopt, CATCH assigns over the pointer, FINISH in a void helper still parses its return, and an unhandled error now terminates from the collector's thread). Sharing keeps the "cannot" bullet, narrowed to what it actually is. err_threads_handoff.c proves it: the existing thread tests all keep every context on the thread that raised it, so the transfer path was exercised nowhere. Seven producers hand errors to one collector through a bounded mutex/condvar queue -- the mutex is the thing under test, since it is what publishes the unlocked content writes -- and the collector asserts the context is still a live slot at refcount 1, that message and trace arrive whole and in each producer's order, that the slot was never recycled in flight, and that a thread which never called akerr_next_error() can release it. A second phase reads a context whose raising thread has already exited. Also document why copying a context by assignment is silently wrong: stacktracebufptr is self-referential, so the copy's cursor points into the source's buffer and the first append corrupts a slot the copier no longer owns. TODO.md records the akerr_copy_error() shape that would fix it and the trigger for building it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 16:35:18 -04:00
Split the README reference material into docs/ 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>
2026-08-01 16:56:07 -04:00
| Document | What it answers |
| -------- | --------------- |
| [docs/architecture.md](docs/architecture.md) | What an error context is, how one travels up the call stack, and what the macros build |
| [docs/usage.md](docs/usage.md) | The macro reference: `ATTEMPT`/`CLEANUP`/`PROCESS`/`FINISH`, `CATCH`, `FAIL_*`, `PASS`, `HANDLE`, `SUCCEED_RETURN` |
| [docs/status-codes.md](docs/status-codes.md) | Defining your own status codes, and reserving a range so two libraries cannot collide |
| [docs/uncaught-errors.md](docs/uncaught-errors.md) | `AKERR_NOIGNORE`, what a stack trace looks like, and how to read one |
| [docs/exit-status.md](docs/exit-status.md) | Why you call `akerr_exit()` and never `exit()`, and how to replace the unhandled-error handler |
| [docs/thread-safety.md](docs/thread-safety.md) | What thread safety here covers, what it does not, and how to hand an error to another thread |
| [docs/building.md](docs/building.md) | Configure options, the generated header, and building without stdlib |
| [UPGRADING.md](UPGRADING.md) | What changed in 1.0.0, 2.0.0 and 2.0.1, and how to migrate |
| [TODO.md](TODO.md) | Known defects, ordered by blast radius |
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
# Installation
```bash
cmake -S . -B build
cmake --build build
cmake --install build
```
Split the README reference material into docs/ 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>
2026-08-01 16:56:07 -04:00
The library depends on `stdlib` and on POSIX threads. Both are optional at the
cost of some functionality — see [docs/building.md](docs/building.md) for
`-DAKERR_USE_STDLIB=OFF` and
[docs/thread-safety.md](docs/thread-safety.md#building-single-threaded) for
`-DAKERR_THREADS=none`.
## Setting up your project
Include it
```c
#include <akerror.h>
```
Link the library directly, or
```sh
cc -lakerror
```
Using pkg-config, or
```sh
pkg-config akerror --cflags
pkg-config akerror --ldflags
```
Using cmake:
```cmake
find_package(akerror REQUIRED)
pkg_check_modules(akerror REQUIRED akerror)
target_link_libraries(YOUR_TARGET PRIVATE akerror::akerror)
```
Using this project as a submodule with cmake:
```cmake
add_subdirectory(deps/libakerror EXCLUDE_FROM_ALL)
target_link_libraries(YOUR_PROJECT PRIVATE akerror::akerror)
```
Split the README reference material into docs/ 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>
2026-08-01 16:56:07 -04:00
# Quickstart
2025-07-21 07:19:41 -04:00
Split the README reference material into docs/ 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>
2026-08-01 16:56:07 -04:00
A function that can fail returns an `akerr_ErrorContext *` instead of a value,
and moves its real output to a pointer parameter. `AKERR_NOIGNORE` makes the
compiler complain if a caller throws that return away.
2025-07-20 21:44:17 -04:00
2025-07-21 07:19:41 -04:00
```c
Split the README reference material into docs/ 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>
2026-08-01 16:56:07 -04:00
#include <akerror.h>
#include <stdio.h>
Split the README reference material into docs/ 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>
2026-08-01 16:56:07 -04:00
/* Fails with a message; the caller finds out what and where. */
static akerr_ErrorContext AKERR_NOIGNORE *open_config(const char *path, FILE **dest)
{
PREPARE_ERROR(errctx);
Split the README reference material into docs/ 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>
2026-08-01 16:56:07 -04:00
FAIL_ZERO_RETURN(errctx, (path != NULL), AKERR_NULLPOINTER,
"no config path was given");
Split the README reference material into docs/ 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>
2026-08-01 16:56:07 -04:00
*dest = fopen(path, "r");
FAIL_ZERO_RETURN(errctx, (*dest != NULL), AKERR_IO,
"could not open %s", path);
Split the README reference material into docs/ 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>
2026-08-01 16:56:07 -04:00
SUCCEED_RETURN(errctx);
2025-07-21 07:19:41 -04:00
}
Split the README reference material into docs/ 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>
2026-08-01 16:56:07 -04:00
int main(int argc, char **argv)
{
FILE *config = NULL;
2025-07-21 07:19:41 -04:00
Split the README reference material into docs/ 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>
2026-08-01 16:56:07 -04:00
PREPARE_ERROR(errctx);
ATTEMPT {
FAIL_ZERO_BREAK(errctx, (argc == 2), AKERR_VALUE,
"usage: %s <config>", argv[0]);
CATCH(errctx, open_config(argv[1], &config));
/* ... read the config ... */
} CLEANUP {
/* Runs whether or not anything failed. */
if ( config != NULL ) {
fclose(config);
}
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_VALUE) {
/* A usage error is ours to handle, so handle it and carry on. */
} FINISH_NORETURN(errctx);
return 0;
2025-07-21 07:19:41 -04:00
}
```
Split the README reference material into docs/ 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>
2026-08-01 16:56:07 -04:00
`ATTEMPT` is where work that can fail goes. `CLEANUP` always runs. `PROCESS`
opens the handler section, and each `HANDLE` claims one status. Anything no
`HANDLE` claims is still an error when it reaches `FINISH`: inside a function,
`FINISH(errctx, true)` returns it to your caller; at the top, as above,
`FINISH_NORETURN(errctx)` prints the stack trace and ends the process. You do
not need to call `akerr_init()` — every entry point does it for you.
2025-07-21 07:19:41 -04:00
Split the README reference material into docs/ 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>
2026-08-01 16:56:07 -04:00
Three things worth knowing before you write much more than that:
2025-07-21 07:19:41 -04:00
Split the README reference material into docs/ 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>
2026-08-01 16:56:07 -04:00
* [The full macro reference](docs/usage.md), including `PASS` for errors you
cannot do anything about, and `HANDLE_GROUP` for several statuses that fail
the same way.
* [Never use `CATCH` or a `FAIL_*_BREAK` macro inside a loop](docs/usage.md#important-do-not-use-catch-or-fail__break-inside-a-loop).
They leave the `ATTEMPT` with a C `break`, which only escapes the innermost
loop. This is the first thing that bites people.
* [Never call `exit()` with a status. Call `akerr_exit()`](docs/exit-status.md).
An exit status is a byte and every consumer status starts at 256, so `exit()`
truncates status 256 to 0 and reports success.
2025-07-21 07:19:41 -04:00
Split the README reference material into docs/ 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>
2026-08-01 16:56:07 -04:00
# Thread safety
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
Split the README reference material into docs/ 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>
2026-08-01 16:56:07 -04:00
The library is thread safe as built by default: every entry point may be called
from any thread at any time, and an error context may be handed from one thread
to another and released there. There is one recursive lock covering the pool and
the registry, so error construction is serialized — a program that raises errors
on its hot path will feel it. See [docs/thread-safety.md](docs/thread-safety.md)
for what that covers, what it cannot, and the handoff pattern.
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
Split the README reference material into docs/ 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>
2026-08-01 16:56:07 -04:00
# Upgrading
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
Split the README reference material into docs/ 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>
2026-08-01 16:56:07 -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
[docs/exit-status.md](docs/exit-status.md). No ABI break.
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
Split the README reference material into docs/ 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>
2026-08-01 16:56:07 -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.
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.