Agent Skill
2/7/2026

errors

Error Handling skill for the ikigai project

M
mgreenly
1GitHub Stars
1Views
npx skills add mgreenly/ikigai

SKILL.md

Nameerrors
DescriptionError Handling skill for the ikigai project

name: errors description: Error Handling skill for the ikigai project

Error Handling

Three Mechanisms

MechanismWhenCompiles Out?
res_tIO, parsing, external failuresNo
assert()Preconditions, contractsYes (-DNDEBUG)
PANIC()OOM, corruption, impossible statesNo

Decision Framework

  1. OOM?PANIC()
  2. Can happen with correct code?res_t
  3. Precondition/contract?assert()
  4. Impossible state?PANIC()

Return Patterns

  1. res_t - Failable operations (IO, parsing)
  2. Direct pointer - Simple creation (PANICs on OOM)
  3. void - Cannot fail
  4. Primitive - Queries (size, bool checks)
  5. Raw pointer - Into buffer (use _ptr suffix)
  6. 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

CodeValueUsage
OK0Success/no error
ERR_INVALID_ARG1Invalid argument validation
ERR_OUT_OF_RANGE2Out of range values
ERR_IO3File operations, config loading
ERR_PARSE4JSON/protocol parsing
ERR_DB_CONNECT5Database connection failures
ERR_DB_MIGRATE6Database migration failures
ERR_OUT_OF_MEMORY7Memory allocation failures
ERR_AGENT_NOT_FOUND8Agent not found in array
ERR_PROVIDER9Provider error
ERR_MISSING_CREDENTIALS10Missing credentials
ERR_NOT_IMPLEMENTED11Not 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:

  • debug build (default): asserts are ACTIVE → violation triggers SIGABRT
  • release build (-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 functionsassert() 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

CodeMarker
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:

  1. The error must be allocated on a context that survives the free
  2. Prefer Option A (pass parent for error allocation)
  3. 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

Skills Info
Original Name:errorsAuthor:mgreenly