errors
Error Handling skill for the ikigai project
Installation
Follow the repository instructions for this skill and your agent. Review the files and dependencies before adding it to your workflow.
View source ↗GuidesSkill instructions
Error Handling
Three Mechanisms
| Mechanism | When | Compiles Out? |
|---|---|---|
res_t |
IO, parsing, external failures | No |
assert() |
Preconditions, contracts | Yes (-DNDEBUG) |
PANIC() |
OOM, corruption, impossible states | No |
Decision Framework
-
OOM? →
PANIC() -
Can happen with correct code? →
res_t -
Precondition/contract? →
assert() -
Impossible state? →
PANIC()
Return Patterns
- res_t - Failable operations (IO, parsing)
- Direct pointer - Simple creation (PANICs on OOM)
- void - Cannot fail
- Primitive - Queries (size, bool checks)
-
Raw pointer - Into buffer (use
_ptrsuffix) - Callback - res_t for events, value for queries, void for side-effects
Core Types
typedef struct {
union { void *ok; err_t *err; };
bool is_err;
} res_t;
typedef struct err {
err_code_t code;
const char *file;
int32_t line;
char msg[256];
} err_t;
Error Codes
| Code | Value | Usage |
|---|---|---|
OK |
0 | Success/no error |
ERR_INVALID_ARG |
1 | Invalid argument validation |
ERR_OUT_OF_RANGE |
2 | Out of range values |
ERR_IO |
3 | File operations, config loading |
ERR_PARSE |
4 | JSON/protocol parsing |
ERR_DB_CONNECT |
5 | Database connection failures |
ERR_DB_MIGRATE |
6 | Database migration failures |
ERR_OUT_OF_MEMORY |
7 | Memory allocation failures |
ERR_AGENT_NOT_FOUND |
8 | Agent not found in array |
ERR_PROVIDER |
9 | Provider error |
ERR_MISSING_CREDENTIALS |
10 | Missing credentials |
ERR_NOT_IMPLEMENTED |
11 | Not implemented |
Macros
-
OK(value)/ERR(ctx, CODE, "msg", ...)- Create results -
TRY(expr)- Extract value or return error -
CHECK(res)- Propagate error if failed -
is_ok(&res)/is_err(&res)- Inspect
Assertions
Build behavior:
-
debugbuild (default): asserts are ACTIVE → violation triggersSIGABRT -
releasebuild (-DNDEBUG): asserts are COMPILED OUT → violation causes undefined behavior (segfault, corruption, etc.)
We normally run debug builds. When you see assert(x) fail, expect SIGABRT, not a segfault.
Guidelines:
- Mark with
// LCOV_EXCL_BR_LINE - Test both paths (pass + SIGABRT)
- Split compound assertions
- Assert side-effect free
PANIC Usage
// OOM - most common
if (ptr == NULL) PANIC("Out of memory"); // LCOV_EXCL_BR_LINE
// Switch default
default: PANIC("Invalid state"); // LCOV_EXCL_LINE
// Corruption
if (size > capacity) PANIC("Array corruption"); // LCOV_EXCL_LINE
Trust Boundary
-
User input → validate exhaustively with
res_t, never crash -
Internal functions →
assert()preconditions, trust caller validated
Testing
- Assertions:
#ifndef NDEBUG+tcase_add_test_raise_signal(tc, test, SIGABRT) - PANIC:
tcase_add_test_raise_signal(tc, test, SIGABRT)(all builds) - OOM: Cannot be tested (process terminates)
Coverage Exclusions
| Code | Marker |
|---|---|
| Assertions | // LCOV_EXCL_BR_LINE |
| OOM checks | // LCOV_EXCL_BR_LINE |
| PANIC logic errors | // LCOV_EXCL_LINE |
New exclusions require updating LCOV_EXCL_COVERAGE in Makefile.
Error Context Lifetime (Critical Rule)
THE TRAP: Errors allocated on a context that gets freed become use-after-free bugs.
// BROKEN - error is allocated on foo, then foo is freed
res_t ik_foo_init(void *parent, foo_t **out) {
foo_t *foo = talloc_zero_(parent, sizeof(foo_t));
res_t result = ik_bar_init(foo, &foo->bar); // Error allocated on foo
if (is_err(&result)) {
talloc_free(foo); // FREES THE ERROR TOO!
return result; // USE-AFTER-FREE CRASH
}
}
THE FIX: Error allocation context must survive the error's return. Either:
Option A (Preferred): Pass parent to sub-functions for error allocation:
res_t ik_foo_init(void *parent, foo_t **out) {
bar_t *bar = NULL;
res_t result = ik_bar_init(parent, &bar); // Error on parent - survives!
if (is_err(&result)) return result;
foo_t *foo = talloc_zero_(parent, sizeof(foo_t));
talloc_steal(foo, bar);
foo->bar = bar;
*out = foo;
}
Option B (Band-aid): Reparent error before freeing context:
res_t ik_foo_init(void *parent, foo_t **out) {
foo_t *foo = talloc_zero_(parent, sizeof(foo_t));
res_t result = ik_bar_init(foo, &foo->bar);
if (is_err(&result)) {
talloc_steal(parent, result.err); // Save error to survivor
talloc_free(foo);
return result;
}
}
Rule: When returning an error after freeing a context:
- The error must be allocated on a context that survives the free
- Prefer Option A (pass parent for error allocation)
- Use Option B (talloc_steal) as fallback
Reference: See fix.md and project/error_handling.md#error-context-lifetime-critical for detailed analysis.
References
Full details: project/return_values.md, project/error_handling.md, project/error_patterns.md, project/error_testing.md
Examples
Read the usage examples in the source repository.
View source ↗Source history
- Indexed on
- 2026-01-20
- Index updated
- 2026-01-26
- Branch
- main
Index and sync dates describe this directory. View the source repository for code history.
GitHub ↗chat Comments (0)
Sign in to join the discussion and leave a comment.
No comments yet. Be the first to share your thoughts!