Claude Code Plugins

Community-maintained marketplace

Feedback

Error Handling skill for the ikigai project

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name errors
description Error Handling skill for the ikigai project

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

  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

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:

  • 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

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:

  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