Frontend Labs
⋯

56. Non-local jumps: setjmp/longjmp and why they're not exceptions

56.1 The mechanism

setjmp saves the current stack context into a jmp_buf; longjmp restores it, making execution jump back to exactly that setjmp call — as if it were returning again, but with the value you pass to longjmp instead of 0. It's the only way in C to unwind multiple stack frames at once without every intermediate function explicitly checking and returning an error code.

#include <setjmp.h>
#include <stdio.h>

static jmp_buf recovery_point;

void deeply_nested(int depth, int fail_at) {
    if (depth == fail_at) {
        longjmp(recovery_point, 1);   // jump straight back to setjmp, skipping every return
    }
    printf("entering depth %d\n", depth);
    deeply_nested(depth + 1, fail_at);
    printf("leaving depth %d\n", depth);   // never runs for frames at or past fail_at
}

int main(void) {
    if (setjmp(recovery_point) == 0) {
        deeply_nested(0, 3);
        printf("this line never runs\n");
    } else {
        printf("recovered: error occurred at depth 3\n");
    }
    return 0;
}
// entering depth 0 / entering depth 1 / entering depth 2 / recovered: error occurred at depth 3
// — notice NO "leaving depth" lines print at all; longjmp skipped every one of those returns.

56.2 The one rule that isn't optional: mark shared locals volatile

#include <setjmp.h>
#include <stdio.h>

static jmp_buf recovery_point;

void risky(volatile int *counter) {
    (*counter)++;
    longjmp(recovery_point, 1);
}

int main(void) {
    volatile int counter = 0;   // MUST be volatile: written before longjmp, read after it —
                                  // without volatile, the compiler may keep counter in a
                                  // register that the stack-rewind doesn't restore, so the
                                  // increment could appear to vanish after the jump
    if (setjmp(recovery_point) == 0) {
        risky(&counter);
    } else {
        printf("counter after longjmp: %d\n", counter);   // reliably 1, because of volatile
    }
    return 0;
}

This is the other legitimate use of volatile alongside Part IX §49.1's hardware registers: any local variable that's modified between setjmp and a longjmp that returns to it, and then read afterward, needs volatile — otherwise the C standard explicitly leaves its value after the jump indeterminate.

56.3 What it doesn't do: no cleanup, no destructors, no stack unwinding of resources

void process(void) {
    FILE *f = fopen("data.txt", "r");
    parse(f);        // if parse() longjmps out on a syntax error...
    fclose(f);        // ...this line never runs — the FILE* leaks, exactly like an
}                       // error-path leak (§44.1), just triggered by a jump instead of a return

C has no equivalent of C++ destructors or defer — a longjmp that skips over a scope skips everything in it, including any pending free/fclose/pthread_mutex_unlock. Anything a longjmp might jump over needs its own goto cleanup-style handling (§4.2) reachable before the jump, or must be tracked and released at the recovery point itself. This is precisely why real interpreters that use setjmp/longjmp for error recovery (many do, including reference implementations of scripting languages) pair it with an explicit resource-tracking stack that the recovery handler walks and releases — longjmp itself will not do it for you.

56.4 sigsetjmp/siglongjmp: the signal-safe variant

Plain setjmp/longjmp don't reliably save/restore the process's signal mask across the jump. If you're recovering from inside a signal handler (Section 55.5's pattern), use sigsetjmp(buf, 1) / siglongjmp instead — the 1 tells it to also save the current signal mask, so siglongjmp correctly restores which signals were blocked/unblocked at the time, instead of potentially leaving a signal masked forever after the jump.


Generated from real-life-c-guide.md · 8 Oct 2026 16:36