C setjmp and longjmp — Non-Local Jumps for Error Recovery
In this tutorial, you will learn about C setjmp and longjmp. We cover key concepts, practical examples, and best practices to help you master this topic.
C setjmp and longjmp provide a non-local jump mechanism that saves the execution environment (stack pointer, frame pointer, registers) with setjmp and restores it later with longjmp, enabling jumps up the call stack without normal return sequences.
What You Will Learn
- How setjmp saves the execution environment
- How longjmp restores it, jumping back multiple stack frames
- Using setjmp/longjmp for deep error recovery
- The limitations and dangers of non-local jumps
- Implementing simple cooperative multitasking
- Volatile variables and their importance across jumps
Why It Matters
Deeply nested function calls in C often need a way to abort an entire operation on error. Consider a parser that calls lexer -> tokenizer -> file reader. If the file reader encounters an I/O error, returning error codes through each layer adds complexity and performance overhead. setjmp/longjmp lets you jump directly from the error to a recovery point, bypassing all intermediate functions. This technique is used in the Lua interpreter's error handling, JPEG library's error recovery, and Durga Antivirus Pro's archive extraction handlers where a corrupt nested archive must abort the entire extraction.
Real-World Use
A ZIP file extractor opens an archive and begins extracting nested files. If the third level of compressed data is corrupted, the extraction must abort entirely, clean up temporary files, and report the error. Without setjmp/longjmp, each function must check and propagate error codes up three levels. With setjmp/longjmp, the error handler jumps directly back to the extraction entry point.
Learning Path
flowchart LR A[Function Pointers] --> B[setjmp & longjmp\nYou are here] B --> C[Assertions] style B fill:#f90,color:#fff
Basic setjmp/longjmp
The setjmp macro saves the call environment into a jmp_buf and returns 0 on the initial call. A subsequent longjmp restores that environment and makes setjmp return a non-zero value.
#include <stdio.h>
#include <setjmp.h>
jmp_buf env;
void inner_function(void) {
printf(" Inside inner_function\n");
longjmp(env, 42); // Jump back to setjmp, return value 42
printf(" This line never executes\n");
}
void middle_function(void) {
printf(" Inside middle_function, calling inner\n");
inner_function();
printf(" This line never executes\n");
}
int main() {
int result = setjmp(env);
if (result == 0) {
printf("First call to setjmp returns 0\n");
printf("Calling middle_function\n");
middle_function();
printf("This line never executes\n");
} else {
printf("Returned from longjmp with value: %d\n", result);
}
return 0;
}
Output:
First call to setjmp returns 0
Calling middle_function
Inside middle_function, calling inner
Inside inner_function
Returned from longjmp with value: 42
The key observation: setjmp returns twice. The first time with 0 (saving the context), the second time with the value passed to longjmp (restoring the context and jumping back).
Error Recovery Pattern
The canonical use case is error recovery across deep call stacks:
#include <stdio.h>
#include <setjmp.h>
#include <stdlib.h>
jmp_buf error_env;
typedef enum {
ERR_NONE,
ERR_FILE_NOT_FOUND,
ERR_PARSE_ERROR,
ERR_OUT_OF_MEMORY,
} ErrorCode;
void parse_file(const char *filename) {
printf(" Opening %s...\n", filename);
// Simulate a parsing error at depth
printf(" Parsing content...\n");
// Error occurs:
longjmp(error_env, ERR_PARSE_ERROR);
printf(" This never prints\n");
}
void process_data(const char *filename) {
printf(" Processing data from %s\n", filename);
// Simulate some preprocessing
parse_file(filename);
printf(" This never prints\n");
}
int main() {
ErrorCode err = setjmp(error_env);
if (err == ERR_NONE) {
// Normal execution path
printf("Starting data processing\n");
process_data("config.txt");
printf("Processing completed successfully\n");
} else {
// Error recovery path
printf("Error occurred: ");
switch (err) {
case ERR_FILE_NOT_FOUND:
printf("File not found\n");
break;
case ERR_PARSE_ERROR:
printf("Parse error\n");
break;
case ERR_OUT_OF_MEMORY:
printf("Out of memory\n");
break;
default:
printf("Unknown error\n");
}
printf("Recovery complete -- continuing main loop\n");
}
return 0;
}
Output:
Starting data processing
Processing data from config.txt
Opening config.txt...
Parsing content...
Error occurred: Parse error
Recovery complete -- continuing main loop
Resource Cleanup with longjmp
Longjmp skips intermediate stack frames, which means destructors or cleanup code in those functions is skipped. You must handle cleanup explicitly:
#include <stdio.h>
#include <setjmp.h>
#include <stdlib.h>
jmp_buf env;
typedef struct {
const char *name;
int is_open;
} Resource;
Resource* open_resource(const char *name) {
Resource *r = malloc(sizeof(Resource));
r->name = name;
r->is_open = 1;
printf(" Opened resource: %s\n", name);
return r;
}
void close_resource(Resource *r) {
if (r && r->is_open) {
printf(" Closed resource: %s\n", r->name);
r->is_open = 0;
free(r);
}
}
void process_with_cleanup(void) {
Resource *r1 = open_resource("database");
Resource *r2 = open_resource("network");
// Error occurs -- need to clean up before longjmp
printf(" Error detected, cleaning up...\n");
close_resource(r1);
close_resource(r2);
longjmp(env, 1);
}
int main() {
if (setjmp(env) == 0) {
process_with_cleanup();
} else {
printf("Recovered from error\n");
}
return 0;
}
Output:
Opened resource: database
Opened resource: network
Error detected, cleaning up...
Closed resource: database
Closed resource: network
Recovered from error
The Volatile Problem
Variables modified between setjmp and longjmp may have indeterminate values after the jump if they are not declared volatile:
#include <stdio.h>
#include <setjmp.h>
jmp_buf env;
void modify_values(void) {
longjmp(env, 1);
}
int main() {
int normal_var = 10;
volatile int vol_var = 20;
if (setjmp(env) == 0) {
normal_var = 100; // Modified after setjmp
vol_var = 200; // Modified after setjmp
modify_values();
} else {
// After longjmp: normal_var may be 10 OR 100 (undefined)
// vol_var is guaranteed to be 200
printf("normal_var = %d (may be wrong!)\n", normal_var);
printf("volatile_var = %d (correct)\n", vol_var);
}
return 0;
}
Any local variable that is modified between setjmp and longjmp must be declared volatile to guarantee its value after the jump.
Coroutine Simulation
A pair of setjmp/longjmp buffers can simulate cooperative multitasking:
#include <stdio.h>
#include <setjmp.h>
jmp_buf main_env, coro_env;
void coroutine(void) {
printf("Coroutine: step 1\n");
if (setjmp(coro_env) == 0) longjmp(main_env, 1);
printf("Coroutine: step 2\n");
if (setjmp(coro_env) == 0) longjmp(main_env, 2);
printf("Coroutine: step 3\n");
longjmp(main_env, 0); // Signal done
}
int main() {
printf("Main: starting coroutine\n");
int state = setjmp(main_env);
if (state == 0) {
// First call -- enter coroutine
coroutine();
}
while (state > 0) {
printf("Main: back from coroutine (state=%d)\n", state);
longjmp(coro_env, 1); // Resume coroutine
state = setjmp(main_env);
}
printf("Main: coroutine finished\n");
return 0;
}
Output:
Main: starting coroutine
Coroutine: step 1
Main: back from coroutine (state=1)
Coroutine: step 2
Main: back from coroutine (state=2)
Coroutine: step 3
Main: coroutine finished
Common Mistakes
Calling longjmp after the function that called setjmp has returned: If the function that called setjmp has already returned, the stack frame is gone. longjmp into it corrupts memory and crashes. This is the most dangerous longjmp pitfall.
Not using volatile for modified locals: Any local variable in the setjmp function that is modified between setjmp and longjmp must be volatile. Otherwise its value after longjmp is undefined.
Skipping resource cleanup: Longjmp skips destructor-like cleanup in intermediate functions. Always use manual cleanup before calling longjmp, or use the cleanup pattern shown above.
Using longjmp in C++: Longjmp does not call C++ destructors for stack-allocated objects. Mixing setjmp/longjmp with C++ is almost always wrong.
Passing 0 to longjmp: If you call longjmp with a value of 0, setjmp still returns 1 (not 0). This prevents infinite loops. If you need 0 semantics, pass a non-zero value and check it.
Portability of jmp_buf: The jmp_buf type is platform-specific. You cannot save a jmp_buf to a file and restore it on a different run or different system.
Signal handling with longjmp: Jumping out of a signal handler with longjmp is technically undefined, though it works on many Unix systems. Avoid this pattern.
Practice Questions
- What happens if you call longjmp after the function containing setjmp has returned?
- Why must local variables modified between setjmp and longjmp be declared volatile?
- How does setjmp/longjmp differ from goto in terms of stack frame management?
- What is the maximum value longjmp can pass to setjmp?
- Challenge: Implement a simple Exception Handling system with try/catch/throw using setjmp/longjmp. Define a
trymacro that calls setjmp, athrow(err)macro that calls longjmp, and acatch(err)block that checks the error code.
Mini Project
Build a safely-callable recursive descent JSON parser that uses setjmp/longjmp for error reporting:
- Define a
ParseContextstruct containing ajmp_buf, the input string, and current position - Write
parse_value,parse_object,parse_array,parse_string,parse_numberfunctions that all accept aParseContext* - On any parse error (unexpected character, malformed number, unclosed string), call
longjmpwith an error code - The top-level
json_parsefunction calls setjmp and enters the parser; on error it returns NULL and fills an error message - Test with valid JSON, invalid JSON (missing comma), and truncated JSON (unexpected end of input)
FAQ
What is Next
Proceed to Assertions to learn how to use assert macros for runtime debugging and invariant checking. Then explore The Preprocessor for compile-time Code Generation.