orelse
Inline failure handling — check a value and bail in the same expression. Works with any scalar type where zero means failure.
Overview
Almost every C function that returns a resource has the same pattern: call, null-check, bail, cleanup. With multiple resources, every new error path must manually release everything acquired so far. Miss one exit and you leak.
orelse collapses the check-and-bail into the declaration. Combined with defer, each acquisition becomes a single self-contained line.
int compile(const char *path) {
FILE *f = fopen(path, "r");
if (!f) return -1;
char *src = read_file(f);
if (!src) {
fclose(f);
return -1;
}
Token *tok = tokenize(src);
if (!tok) {
free(src);
fclose(f);
return -1;
}
int r = emit(tok);
token_free(tok);
free(src);
fclose(f);
return r;
}
int compile(const char *path) {
FILE *f = fopen(path, "r")
orelse return -1;
defer fclose(f);
char *src = read_file(f)
orelse return -1;
defer free(src);
Token *tok = tokenize(src)
orelse return -1;
defer token_free(tok);
return emit(tok);
}
Every early return automatically runs all registered defers — no manual cleanup needed at any exit point.
Forms
orelse has several forms depending on what you want to do on failure:
| Form | Example | Meaning |
|---|---|---|
| Control flow | x = f() orelse return -1; | Return, break, continue, or goto on failure |
| Block | x = f() orelse { log(); return -1; } | Run arbitrary code on failure |
| Fallback value | x = f() orelse default_val | Use a default value if falsy |
| Bare expression | do_init() orelse return -1; | Check without assignment |
| Array dimension | int buf[n orelse 1] | Safe default for VLA dimensions |
Behavior
orelse checks if the result is falsy — zero integer, null pointer, or 0.0 float. Negative values (-1, -EINVAL) are truthy and do not trigger. Use orelse for null/zero checks, not for negative error codes.
The assignment happens inside the condition to avoid reading the variable twice — safe for volatile targets:
// x = f() orelse return -1 emits:
{ if (!(x = f())) { return -1; } }
When the action is a return, all active defers in scope run first — exactly like a normal return.
orelse is not short-circuit: the left-hand expression is always fully evaluated. Side effects always occur.
Examples
Null pointer check
char *buf = malloc(size) orelse return -ENOMEM;
defer free(buf);
File open with error logging
FILE *f = fopen(path, "r") orelse {
fprintf(stderr, "cannot open %s: %s
", path, strerror(errno));
return -1;
}
defer fclose(f);
File descriptor
int fd = open(path, O_RDONLY) orelse return -1;
defer close(fd);
Integer zero check
size_t n = fread(buf, 1, len, f) orelse break; // 0 bytes = EOF or error
Fallback value
const char *home = getenv("HOME") orelse "/tmp";
int workers = get_cpu_count() orelse 1;
Chained acquisition with goto cleanup
int setup(struct App *app) {
app->db = db_open("app.db") orelse goto fail_db;
app->net = net_init() orelse goto fail_net;
app->ui = ui_create() orelse goto fail_ui;
return 0;
fail_ui: net_shutdown(app->net);
fail_net: db_close(app->db);
fail_db: return -1;
}
Bare expression check
// orelse on expressions that return NULL or 0 on failure
setlocale(LC_ALL, "") orelse return -1;
chdir("/tmp") orelse return -1; // 0 = success, -1 = failure
VLA safe dimension
int n = get_count();
int buf[n orelse 1]; // avoids zero-length VLA if n == 0
Patterns & Tricks
Chained orelse — fallback chain
Use chained orelse to try multiple fallbacks in order:
const char *config_path =
getenv("MYAPP_CONFIG") orelse
getenv("XDG_CONFIG_HOME") orelse
"~/.config/myapp.conf";
The first non-zero result is used; the rest are skipped.
Declaration-init orelse — safe defaults
Use orelse in variable initialization to provide safe defaults:
int max_workers = get_cpu_count() orelse 4;
size_t buffer_size = get_config_bufsize() orelse (64 * 1024);
Volatile MMIO safety — register reads without double-fetch
Use orelse on volatile register reads. The assignment-in-condition pattern ensures the register is read exactly once:
volatile uint32_t *status_reg = (...);
uint32_t status = *status_reg; orelse 0xFF; // safe: reg read once
if (status & READY_BIT) {
handle_ready();
}
Combined with defer — acquire-check-defer pattern
Pair orelse with defer for a single-line acquire-and-register-cleanup:
int fd = open(path, O_RDWR) orelse return -1;
defer close(fd); // fd is now guaranteed to be closed on any exit
setup(fd);
This is the core pattern: acquire with orelse, immediately follow with defer cleanup.
Edge Cases
- Negative values do not trigger.
-1 orelse ...does not fire —!(-1)is false. Useorelsefor null/zero, not errno-style negative codes. - Struct values are rejected.
orelseon a struct value is a compile error. Struct pointers work fine. - Not short-circuit. The expression is always fully evaluated before the check.
- Static / extern variables.
orelsein astaticorexterninitializer is a compile error — the transformation requires a runtime assignment, which breaks static initialization semantics. - Constant expression contexts.
orelseinsideenumconstants,_BitInt(N), or_Alignas(N)is rejected — these require compile-time constants. - VLA dimension side effects.
orelsein array size brackets rejects function calls,++/--, assignments, and volatile reads — the dimension expression is hoisted to a temp variable, which would cause double evaluation.