Document that CATCH/FAIL_*_BREAK must not be used inside a loop
These macros leave the ATTEMPT block with a C break, which only escapes the innermost loop/switch. Nesting them in a loop inside an ATTEMPT lets the rest of the block run with an error already pending. Document the correct patterns: iterate with PASS / FAIL_*_RETURN (which return, not break), or move the loop into a helper returning akerr_ErrorContext * and CATCH the single call. Also note that merely extracting the loop into a function does not fix it if the helper still wraps the loop in ATTEMPT. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
45
README.md
45
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.
|
||||
|
||||
Reference in New Issue
Block a user