diff --git a/README.md b/README.md index c790722..f092510 100644 --- a/README.md +++ b/README.md @@ -210,6 +210,8 @@ ATTEMPT { 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. @@ -232,6 +234,49 @@ ATTEMPT { 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: + +```c +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: + +```c +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: + +```c +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.