# 04. Errors and status codes Every one of libakgl's exported functions returns `akerr_ErrorContext AKERR_NOIGNORE *`. You cannot read a single example in this manual until you can read that return value, which is why this chapter comes before any subsystem. ## The protocol is libakerror's `ATTEMPT` / `CLEANUP` / `PROCESS` / `HANDLE` / `HANDLE_GROUP` / `FINISH`, and the `PASS`, `CATCH`, `IGNORE`, `FAIL_*` and `SUCCEED_RETURN` macros, all belong to **libakerror**. They are documented by the project that owns them, in `deps/libakerror/README.md` — "Library Architecture", "Lifecycle of an error in the AKError library", and "Exit status". This manual does not restate them, because a copy here would be wrong the day libakerror changes and nothing in this repository would notice. What is genuinely libakgl's, and is written down nowhere else, is **which statuses these 156 functions raise and what each one means here**. That is the three tables below. The house rules for *writing* code against the protocol — never a `*_RETURN` inside an `ATTEMPT`, never a bare `return` out of a `HANDLE`, `CLEANUP` before `PROCESS`, an `ATTEMPT` inside the loop rather than a `CATCH` inside one — are in `AGENTS.md` under "Error-Handling Protocol", and two of them are enforced by the `error_protocol` test. ## Table 1 — libakgl's own status codes libakerror reserves statuses 0–255 for the host's `errno` values and its own `AKERR_*` codes; consumers allocate upward from `AKERR_FIRST_CONSUMER_STATUS`, which is **256**. libakgl claims a band of five starting there, under the owner string `"libakgl"`: ```c excerpt=include/akgl/error.h #define AKGL_ERR_OWNER "libakgl" #define AKGL_ERR_BASE AKERR_FIRST_CONSUMER_STATUS #define AKGL_ERR_SDL (AKGL_ERR_BASE + 0) /**< An SDL call failed; the message carries SDL_GetError() */ #define AKGL_ERR_REGISTRY (AKGL_ERR_BASE + 1) /**< A registry property or lookup operation failed */ #define AKGL_ERR_HEAP (AKGL_ERR_BASE + 2) /**< A heap pool has no free object left to hand out */ #define AKGL_ERR_BEHAVIOR (AKGL_ERR_BASE + 3) /**< A component did not behave the way its contract requires */ #define AKGL_ERR_LOGICINTERRUPT (AKGL_ERR_BASE + 4) /**< Actor logic is telling the physics simulator to skip it */ ``` **`AKGL_ERR_LIMIT` is not a status code.** It is `AKGL_ERR_BASE + 5`, one past the last one, and exists only so `AKGL_ERR_COUNT` can be computed from it. Nothing in `src/` raises it and `akgl_error_init` does not name it. Do not write a `HANDLE(e, AKGL_ERR_LIMIT)` arm — it would catch nothing, and if a sixth real code is ever added it would silently start catching that instead. | Code | Value | Means | Raised by | What the caller does | |---|---|---|---|---| | `AKGL_ERR_SDL` | 256 | An SDL call failed; the message carries `SDL_GetError()` | Around forty sites — anything touching a window, texture, renderer, mixer or the state mutex | Usually fatal at startup. Check the driver and the asset path | | `AKGL_ERR_REGISTRY` | 257 | A name lookup against a registry failed | **One site only**: `akgl_controller_default`, when `actorname` is not in `AKGL_REGISTRY_ACTOR` | Bind the control map *after* the actor is initialized | | `AKGL_ERR_HEAP` | 258 | A pool has no free slot | every `akgl_heap_next_*` | **Normally a missing release, not a small pool.** See [Chapter 5](05-the-heap.md) before raising `AKGL_MAX_HEAP_*` | | `AKGL_ERR_BEHAVIOR` | 259 | A component did not behave the way its contract requires | **Nothing in the library.** Only `tests/` raises it, through `testutil.h` | Available to you for the same purpose: asserting a contract in your own code | | `AKGL_ERR_LOGICINTERRUPT` | 260 | **Not a failure — a control signal.** "Skip the rest of this step for this actor" | your own `movementlogicfunc` | Raise it deliberately. See the note below | `akgl_error_init` reserves the band all-or-nothing and registers a name for each of the five: `"SDL Error"`, `"Registry Error"`, `"Heap Error"`, `"Behavior Error"`, `"Logic Interrupt"`. Those names are what a stack trace prints. Two rows deserve more than a cell. **`AKGL_ERR_REGISTRY` is rarer than its name suggests.** The obvious candidates do not raise it. `akgl_actor_initialize` raises `AKERR_KEY` when the registry write fails, and `akgl_actor_set_character` raises `AKERR_NULLPOINTER` when the named character is not in `AKGL_REGISTRY_CHARACTER`. If you are writing a `HANDLE` arm for "that name was not registered", `AKERR_NULLPOINTER` and `AKERR_KEY` are the statuses you will actually see. **`AKGL_ERR_LOGICINTERRUPT` only works from one place.** `akgl_physics_simulate` wraps each actor's step in an `ATTEMPT` and calls `actor->movementlogicfunc` through `CATCH`, with a `HANDLE(errctx, AKGL_ERR_LOGICINTERRUPT)` arm that does nothing — so a raise from your movement logic skips that actor's gravity, drag and move for this step and the loop carries on to the next actor. The backend's own `gravity` and `move` are called through `PASS` in the same block, and `PASS` returns out of the function. **A backend hook that raises `AKGL_ERR_LOGICINTERRUPT` aborts the whole physics step**, leaving every remaining actor unsimulated and `gravity_time` unadvanced. Raise it from `movementlogicfunc` and nowhere else. ## Table 2 — libakerror statuses libakgl raises, and what they mean here The statuses are libakerror's; their libakgl meaning is not. This table was built by grepping every `FAIL_*` and `HANDLE*` site in `src/`, not by reading header prose. | Status | What it means when a libakgl function raises it | |---|---| | `AKERR_NULLPOINTER` | By a wide margin the most common — roughly six of every seven raise sites in `src/`. A required pointer argument was `NULL` — *or* a required field was empty (`akgl_game.name`/`.version`/`.uri`), *or* a name was looked up and not found. `akgl_actor_set_character` and `akgl_get_property` both use it for "absent", and `registry_create` uses it for "SDL could not allocate the property set" | | `AKERR_KEY` | **A key is absent, or a registry write was refused.** A missing JSON object member (every `akgl_get_json_*_value`), a character with no sprite for a state, a physics backend name that is neither `null` nor `arcade`, a failed `SDL_SetPointerProperty` in `akgl_actor_initialize`. Frequently a `HANDLE` arm rather than a failure | | `AKERR_TYPE` | **A JSON value was present but the wrong type.** All of `src/json_helpers.c` and `akgl_get_json_tilemap_property`. This is the status that separates "you did not write that key" (`AKERR_KEY`) from "you wrote it as a string and it wants a number" | | `AKERR_OUTOFBOUNDS` | A value did not fit its destination, or an index ran off the end. `aksl_strncpy` truncation, an array index past the end of a JSON array (`akgl_get_json_array_index_*`), a ninth child on an actor, a tile past `AKGL_TILEMAP_MAX_*`, a control map id out of range | | `AKERR_VALUE` | A value parsed but was not usable: a version string that is not semver (`akgl_game_load_versioncmp`), two surfaces that differ (`akgl_compare_sdl_surfaces`), a zero-sized destination or an overlapping copy from the `aksl_` wrappers | | `AKERR_RELATIONSHIP` | **One site**: `akgl_actor_add_child`, when the child already has a parent. Detach it first | | `AKERR_API` | Two distinct meanings, so read the message. (1) **The function is not implemented** — `akgl_physics_arcade_collide` and `akgl_render_2d_draw_mesh` both `FAIL_RETURN(..., AKERR_API, "Not implemented")`, and both are reached by ordinary-looking calls. (2) **A savegame does not match this build** — `akgl_game_load` and `akgl_game_load_versioncmp` | | `AKERR_IO` | A read or write failed, or a savegame had trailing data after its name tables. Distinct from `AKERR_EOF` — that separation is the whole reason `aksl_fgetc` exists | | `AKERR_EOF` | End of input. Sometimes the *desired* outcome: `require_at_eof` in `src/game.c` handles it and treats anything else as a failure | | `AKERR_INDEX` | **libakgl never raises this.** It appears once, as a `HANDLE_GROUP` arm in `akgl_get_json_with_default`, so that a caller whose *own* code raises it still gets a default. An out-of-range JSON array index arrives as `AKERR_OUTOFBOUNDS` | Statuses raised by the libraries underneath also reach you unchanged. `aksl_fopen` reports `ENOENT` and `EACCES` as themselves; `aksl_fclose` reports `ENOSPC` and `EDQUOT`. `akgl_error_init` can raise libakerror's `AKERR_STATUS_RANGE_OVERLAP`, `AKERR_STATUS_RANGE_FULL` or `AKERR_STATUS_NAME_FULL`. [Chapter 22](22-appendix-limits.md) has the per-function cross-reference. ## Table 3 — the exit status trap This is arithmetic, and it is silent. | You write | Wait status the shell sees | Why | |---|---|---| | `exit(AKGL_ERR_SDL)` | **0 — a clean run** | An exit status is one byte. `AKGL_ERR_SDL` is 256, and 256 & 0xFF is 0 | | `akerr_exit(status)` | 0 → 0, 1–255 → itself, anything else → 125 | 125 is `AKERR_EXIT_STATUS_UNREPRESENTABLE` | Every suite in `tests/` once reported success on the single most common failure a library built on SDL can have — a renderer that would not come up. `tests/character.c` aborted at its second of four tests and was green until 0.5.0. **You do not normally need to do anything about this.** libakerror's default unhandled-error handler already calls `akerr_exit()`, so a `main` that ends in `FINISH_NORETURN` exits correctly. The trap is only live if you replace libakerror's `akerr_handler_unhandled_error` pointer with your own handler, or call `exit(3)` on a status yourself. If you do either, call `akerr_exit()`. ## Four traps that are libakgl's, not libakerror's ### `akgl_error_init()` must run before anything that can raise `akgl_error_init` claims the status band *and* registers the five names. A code raised before it runs has no name in the registry, so every stack trace carrying it prints **"Unknown Error"** instead of "Heap Error" — and the status number, 258, means nothing to a reader. `akgl_game_init` calls it first, before anything else that can fail — that is why the mutex creation and the `akgl_game.name` checks sit *after* it. A host with its own startup path, such as one binding an existing renderer with `akgl_render_2d_bind` instead of calling `akgl_game_init`, must call `akgl_error_init` itself. Repeat calls are a no-op, so a program that cannot order its initialization precisely may simply call it more than once. ### The SDL callbacks end in `FINISH_NORETURN`, and that exits the process `SDL_EnumerateProperties` takes a `void`-returning callback, so a failure inside one has nowhere to go. All six of libakgl's therefore end in `FINISH_NORETURN`, which logs the trace and hands the context to libakerror's unhandled-error handler — **and the default handler exits the process.** They are `akgl_registry_iterate_actor` (`src/actor.c`), `akgl_character_state_sprites_iterate` (`src/character.c`), and the four savegame name iterators in `src/game.c`. A reader meets this the first time a sprite name is wrong: an actor's `renderfunc` raises `AKERR_KEY`, the callback cannot return it, and the game exits mid-frame with a stack trace instead of returning an error you could have handled. Two consequences worth planning for: - A write error part-way through `akgl_game_save` terminates the game rather than reaching `akgl_game_save`'s return value. `game.h` says so on `akgl_game_save_actors`. - If you want a different outcome, point libakerror's `akerr_handler_unhandled_error` function pointer at your own handler — and call `akerr_exit()` from it, per Table 3. Note that `akgl_game_update` does **not** go through the callback. It walks `akgl_heap_actors` directly and propagates through `PASS`, so an error from an actor's `updatefunc` does reach you. The callback path is the render sweep and the savegame writer. ### `akgl_get_json_with_default` hands back the context you gave it This is the one libakgl function whose ownership rule you have to know before you write your first loader. It takes an incoming `akerr_ErrorContext *`, and when it does not handle that status it returns **the same pointer**, because its `FINISH(e, true)` passes `e` up. So the context has exactly one owner at a time, and it is never both yours and its. A `CLEANUP` block that releases your original pointer *as well as* releasing what the call returned releases the same slot twice, and a double-released context corrupts the failure instead of reporting it. That is not hypothetical: it is how the first draft of the `AKERR_OUTOFBOUNDS` test in `tests/json_helpers.c` passed against the unfixed library. `AGENTS.md` documents the shape under "Testing Guidelines"; `TODO.md` item 18 records the history. The correct shape: take the result into a local, `NULL` your own pointer immediately, and decide from the local. It defaults on three statuses — `AKERR_KEY`, `AKERR_OUTOFBOUNDS` and `AKERR_INDEX` — so both a missing object member and a short array give you the default. It does **not** default on `AKERR_TYPE`: a key that is present with the wrong type is a mistake in the document, not an absent value, and it propagates. ### libakerror 2.0.1 is a hard floor, checked at compile time `include/akgl/error.h` carries two `#error` feature tests, because libakerror publishes no version macro and each probes the narrowest thing that a required release introduced: - `AKERR_FIRST_CONSUMER_STATUS` — added in 1.0.0 with the private status registry. Without it, `AKGL_ERR_BASE` does not exist and the failure lands much later, inside `src/heap.c`. - `AKERR_EXIT_STATUS_UNREPRESENTABLE` — added in 2.0.1 with `akerr_exit()`. Both failures are an *installed header* being stale, not a build-tree problem. 2.0.0 is an ABI break — `akerr_next_error()` returns a context that already holds its reference, and `__akerr_last_ignored` is thread-local — and both expand at libakgl's own call sites through `IGNORE` and the `FAIL_*` macros. The soname is `libakerror.so.2`, so the *libraries* cannot be mixed by accident; the header can. Rebuild and reinstall libakerror. ## One worked example A loader reading an optional integer out of a JSON document. It is the smallest piece of real libakgl code that exercises both halves of the chapter: a status that means "absent and that is fine", and the ownership rule on the function that turns it into a default. ```c #include #include #include #include /* Read an optional integer out of a loaded document, defaulting to 4. */ akerr_ErrorContext AKERR_NOIGNORE *read_speed(json_t *doc, int *dest) { akerr_ErrorContext *keyerr = NULL; akerr_ErrorContext *unhandled = NULL; int def = 4; PREPARE_ERROR(errctx); FAIL_ZERO_RETURN(errctx, doc, AKERR_NULLPOINTER, "NULL document"); FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL destination"); keyerr = akgl_get_json_integer_value(doc, "speed", dest); unhandled = akgl_get_json_with_default(keyerr, &def, dest, sizeof(int)); keyerr = NULL; if ( unhandled != NULL ) { return unhandled; } SUCCEED_RETURN(errctx); } int main(void) { json_t *doc = NULL; int speed = 0; PREPARE_ERROR(errctx); ATTEMPT { CATCH(errctx, akgl_error_init()); doc = json_pack("{s:i}", "width", 32); CATCH(errctx, read_speed(doc, &speed)); } CLEANUP { if ( doc != NULL ) { json_decref(doc); } } PROCESS(errctx) { } FINISH_NORETURN(errctx); } ``` Four things in there are libakgl-specific and worth naming: 1. **`akgl_error_init()` is the first call**, before anything that can raise. 2. **`keyerr = NULL;` on the line after the call.** Ownership passed either way — handled or not — and the local is the only surviving reference. Nothing else in the function may touch `keyerr` again. 3. **A `speed` key that is absent is not an error.** `akgl_get_json_integer_value` raises `AKERR_KEY`, `akgl_get_json_with_default` handles it and writes 4. A `speed` key that is present as a string *is* an error: `AKERR_TYPE` is not defaulted and comes back as `unhandled`. 4. **`main` ends in `FINISH_NORETURN`.** That is what gets you `akerr_exit()` and a truthful exit status without writing the trap from Table 3. ## Where to go next - [Chapter 5](05-the-heap.md) — `AKGL_ERR_HEAP`, and why it is almost always a missing release. - [Chapter 6](06-the-registry.md) — the name lookups behind `AKERR_KEY` and `AKERR_NULLPOINTER`. - [Chapter 22](22-appendix-limits.md) — which function raises what. - `deps/libakerror/README.md` — the protocol itself, and the exit-status discussion. - The generated Doxygen reference — every function's own `@throws` list.