← Prism
Docs
Overview defer orelse zeroinit raw auto-unreachable
Spec Draft
GitHub ↗

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.

Standard C — manual checks, manual cleanup
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;
}
With Prism — orelse + defer
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:

FormExampleMeaning
Control flowx = f() orelse return -1;Return, break, continue, or goto on failure
Blockx = f() orelse { log(); return -1; }Run arbitrary code on failure
Fallback valuex = f() orelse default_valUse a default value if falsy
Bare expressiondo_init() orelse return -1;Check without assignment
Array dimensionint 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

Spec Reference

orelse — Prism Spec §6.2