Files
libakerror/docs/usage.md
Andrew Kesterson 5695061130
Some checks failed
libakerror CI Build / cmake_build (push) Successful in 2m47s
libakerror CI Build / coverage (push) Successful in 2m48s
libakerror CI Build / thread_sanitizer (push) Failing after 2m49s
libakerror CI Build / mutation_test (push) Successful in 39m56s
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

10 KiB

Using the library

(Optional) Configuring the logging function

The default logging function (used for logging stack traces on failure) defaults to a wrapper that calls fprintf(stderr, f, ...). If you want to override this behavior, then set the error handler to a function with a printf-style signature:

void my_logger(const char *fmt, ...)
{
	/* ... do something */
}


/* set your custom error handler */
akerr_log_method = &my_logger;
	
/* proceed to use the library */

Setting Up the Error Context

Before you can use any of these macros you must set up an error context inside of the current scope.

PREPARE_ERROR(errctx);

This will create a akerr_ErrorContext structure inside of the current scope named errctx and initialize it. This structure is used for all operations of the library within the current scope. Attempting to use the library in a given scope before calling this will result in compile-time errors.

Attempting an Operation

ATTEMPT {
	// ... code
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true)

ATTEMPT { ... } is the block within which you will perform operations which may cause errors that need to be caught. See "Capturing errors", below.

CLEANUP { ... } is the block within which you will perform any code which MUST be executed REGARDLESS of whether or not errors were thrown. Closing open file handles, or releasing memory, for example.

PROCESS(errctx) { ... } is the block within which you will handle any errors that were caught inside of the ATTEMPT block. See "Handling Errors" below.

FINISH(errctx, true) terminates the attempt operation. The FINISH macro takes two arguments: the name of the akerr_ErrorContext, and a boolean regarding whether or not to pass unhandled errors up to the calling function. Unless you are inside of your main() method, this should be true. Inside of your main() method, call FINISH_NORETURN(errctx) instead.

Capturing errors

Inside of an ATTEMPT block, any operation which could generate or represent an error should be wrapped in one of several macros.

Capturing errors from functions which return akerr_ErrorContext *

For functions that return akerr_ErrorContext *, you should use the CATCH macro.

ATTEMPT {
    CATCH(errctx, errorGeneratingFunction())
} // ...

This will assign the return value of the function in question to the akerr_ErrorContext previously prepared in the current scope. If the function returns an akerr_ErrorContext that indicates any type of error, the ATTEMPT block is immediately exited, and the CLEANUP block begins.

(One caveat: because this exit is implemented with a C break, CATCH must not be used inside a loop within the ATTEMPT block — see the section "Important: do not use CATCH or FAIL_*_BREAK inside a loop" below.)

Setting errors from functions or expressions returning integer

For functions that return integer, such as logical comparisons or most standard library functions, use the FAIL_ZERO_BREAK and FAIL_NONZERO_BREAK macros. These macros allow you to capture an integer return code from an expression or function and set an error code in the current context based off that return.

Here is an example of checking for a NULL pointer

ATTEMPT {
    FAIL_ZERO_BREAK(errctx, (somePointer != NULL), AKERR_NULLPOINTER, "Someone gave me a NULL pointer")
} // ...

Here is an example of checking for two strings that are not equal

ATTEMPT {
    FAIL_NONZERO_BREAK(errctx, strcmp("not", "equal"), AKERR_VALUE, "Strings are not equal")
} // ...

When either of these two macros are used, the ATTEMPT block is immediately exited, and the CLEANUP block begins.

Important: do not use CATCH or FAIL_*_BREAK inside a loop

CATCH, FAIL_ZERO_BREAK, FAIL_NONZERO_BREAK, and FAIL_BREAK leave the ATTEMPT block by executing a C break statement. In C, break only exits the innermost enclosing for, while, do, or switch. Therefore these macros must not be used inside a loop (or a nested switch) that is itself inside an ATTEMPT block. If you do, the break escapes only the loop — not the ATTEMPT — and the rest of the ATTEMPT body then runs with an error already pending:

ATTEMPT {
    for ( int i = 0; i < n; i++ ) {
        CATCH(errctx, process(items[i]));   // WRONG: break exits the for loop, not the ATTEMPT
    }
    // ... this code still executes, with errctx already in an error state ...
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);

Note that moving the loop into a helper function does not fix this on its own — if the helper still wraps the loop in an ATTEMPT and uses CATCH/FAIL_*_BREAK inside it, it has the exact same problem. The fix is to iterate with return-based macros, which are unaffected by loop nesting. Use one of the two patterns below.

Pattern 1 — use PASS (or a FAIL_*_RETURN macro) inside the loop. These exit the enclosing function with a return rather than a break, so loop nesting is irrelevant. Use this when the loop should stop and propagate on the first error:

akerr_ErrorContext AKERR_NOIGNORE *process_all(Item *items, int n)
{
    PREPARE_ERROR(errctx);
    for ( int i = 0; i < n; i++ ) {
        PASS(errctx, process(items[i]));   // returns from process_all on the first error
    }
    SUCCEED_RETURN(errctx);
}

Pattern 2 — move the loop into a helper and CATCH the single call. When you need a CLEANUP block or want to HANDLE the error locally, put the loop in its own akerr_ErrorContext *-returning function (written per Pattern 1) and CATCH that one call. The CATCH is then not inside a loop, so its break scopes to the ATTEMPT correctly:

ATTEMPT {
    CATCH(errctx, process_all(items, n));   // a single CATCH, not looped
} CLEANUP {
    // ... always runs ...
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_VALUE) {
    // ... handle a failure from any iteration ...
} FINISH(errctx, true);

Passing errors

Sometimes you can't actually do anything about the errors that come out of a given method, but you want that error to be propagated back up the call chain, and to be properly reported. If this is your goal, you can avoid using a ATTEMPT ... FINISH block, and simply use the PASS macro.

PREPARE_ERROR(e);
PASS(e, some_method_that_may_fail());
SUCCEED_RETURN(e);

This does the same thing as this, but with less code:

PREPARE_ERROR(e);
ATTEMPT {
    CATCH(e, some_method_that_may_fail());
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);

Handling errors

Inside of the PROCESS { ... } block, you must handle any errors that occurred during the ATTEMPT { ... } block. You do this with HANDLE, HANDLE_GROUP, and HANDLE_DEFAULT.

Handling a specific error with HANDLE

In order to handle a specific error code, use the HANDLE macro.

} PROCESS(errctx) {
} HANDLE(errctx, AKERR_NULLPOINTER) {
    // Something is complaining about a null pointer error. Do something about it.
} // ...

Handling a group of errors with HANDLE_GROUP

In order to handle a group of related errors that all require the same failure behavior, use HANDLE followed by HANDLE_GROUP. For example, to handle a scenario where an IO error, key error, and index error all need to be handled the same way:

} PROCESS(errctx) {
} HANDLE(errctx, AKERR_IO) {
} HANDLE_GROUP(errctx, AKERR_KEY) {
} HANDLE_GROUP(errctx, AKERR_INDEX) {
    // error handling code goes here
}

This creates a fallthrough mechanism where all 3 errors get the same error handling code. Note that while the cases fall through, you can still (if desired) put some code specific to each error in that error's HANDLE or HANDLE_GROUP block; but this is not required, only the final handler needs to get any code.

The fallthrough behavior stops as soon as another HANDLE macro is encountered. For example, in this example, AKERR_IO, AKERR_KEY and AKERR_INDEX are all handled as a group, but AKERR_RELATIONSHIP is not.

} PROCESS(errctx) {
} HANDLE(errctx, AKERR_IO) {
} HANDLE_GROUP(errctx, AKERR_KEY) {
} HANDLE_GROUP(errctx, AKERR_INDEX) {
    // This code handles 3 error cases
} HANDLE(errctx, AKERR_RELATIONSHIP) {
    // This code handles 1 error case
}

Returning success or failure from functions returning akerr_ErrorContext *

If at all possible, when using this library, your functions should return akerr_ErrorContext *. When returning from such functions, you should use the SUCCEED_RETURN and FAIL_RETURN macros.

SUCCEED_RETURN

This macro is used when your function has reached the end of its happy code path and is prepared to exit successfully. This sets the akerr_ErrorContext to a successful state and exits the function.

PREPARE_ERROR(errctx);
ATTEMPT {
    // ... stuff
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);

FAIL_RETURN

If the code path in the current function reaches a state wherein an error must be set and the function must return early, you can use FAIL_RETURN to accomplish this. Note that this should not be used inside of an ATTEMPT { ... } block; this immediately exits the function, preventing a CLEANUP { ... } block from executing. This can be safely used from inside of a CLEANUP or PROCESS block, or from anywhere within the function not inside of an ATTEMPT { ... } block.

The function allows you to provide printf-style variable arguments to provide a meaningful failure message.

PREPARE_ERROR(errctx);
FAIL_RETURN(AKERR_BEHAVIOR, "Something went horribly wrong!")

Conditionally failing and returning

In addition to FAIL_RETURN you can also test for zero or non-zero conditions, set an error, and return from the function immediately. Use the FAIL_ZERO_RETURN and FAIL_NONZERO_RETURN macros for this. These macros can be used anywhere that FAIL_RETURN can be used.

PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, (somePointer != NULL), AKERR_NULLPOINTER, "Someone gave me a NULL pointer")
PREPARE_ERROR(errctx);
FAIL_NONZERO_RETURN(errctx, strcmp("not", "equal"), AKERR_VALUE, "Strings are not equal")