Menu
Try in Playground

Zero Raises and Check: Explicit Failure Without Exceptions

Zero functions declare their failure modes with raises and callers acknowledge them with check. Here's how the system works, why there's no silent throw, and how it interacts with the World capability.

This page includes runnable editors — edit, run, and see output instantly.

The Setup

Failure in Zero isn't a separate, parallel control flow. It's part of the regular control flow, declared on a function's signature and acknowledged at every call site. Two pieces make this work:

  • raises in the function signature — "this function can fail."
  • check at the call site — "if this fails, fail my function with the same error."

The combination is enough to express what most languages reach for try/catch or Result types to handle.

Declaring a Fallible Function

Add raises after the return type:

fun validate(ok: Bool) -> i32 raises { InvalidInput } {
    if ok == false {
        raise InvalidInput
    }
    return 42
}

Two ways to read the signature:

  • "validate returns an i32 or raises InvalidInput."
  • "The set of possible outcomes is { i32, InvalidInput }."

Both readings are correct. The compiler tracks both possibilities and demands that callers do something explicit about each.

You can also write a bare raises clause:

pub fun main(world: World) -> Void raises {
    check world.out.write("hello\n")
}

raises (no error list) means "this function can fail with any error". On main this is the conventional form — the program can exit with a nonzero status if anything goes wrong, and the runtime takes care of surfacing the error.

For functions deeper in the call stack, prefer the explicit form raises { ErrorA, ErrorB } so the failure modes are documented at each level.

Raising an Error

Inside a fallible function, raise exits the function with the given error:

fun validate(ok: Bool) -> i32 raises { InvalidInput } {
    if ok == false {
        raise InvalidInput
    }
    return 42
}

raise InvalidInput produces an InvalidInput error at that line. The function does not continue past that point — control returns to the caller, and the caller sees the error instead of an i32. The raises clause lists the only error types this function is allowed to raise; raising something not in the list is a compile error.

Propagating an Error with check

A caller invoking a fallible function has to acknowledge the possible failure. The most common acknowledgment is check:

fun run() -> Void raises { InvalidInput } {
    check validate(true)
}

check validate(true) does two things:

  1. Calls validate(true).
  2. If validate raised an error, propagates it up — run raises the same error to its caller.

For the propagation to be allowed, run has to declare that it can raise InvalidInput (or something compatible) in its own raises clause. The compiler checks this. If run's signature said raises { OtherError }, the propagation would fail to compile because InvalidInput isn't in the set.

A complete worked example from the language's official samples — click Run to see propagation succeed:

The error type travels with the function signature all the way up the call stack. main's bare raises accepts anything run might raise, so the propagation lands safely.

Why Not try/catch?

The design discipline behind raises/check is that failure is never invisible at a call site. In a try/catch language, an exception can move silently through a function that doesn't even know it might be involved — the function looks pure, but a deep call somewhere in its body throws and the exception unwinds through it.

That's convenient for the author of the throwing code. It's costly for everyone else:

  • Readers (and agents) can't tell from a signature whether a function participates in failure paths.
  • Refactoring becomes nervous — moving a call across functions can change what exceptions are reachable.
  • Recovery code lives far from the place that knows what to do.

Zero pays the cost upfront — annotations on every fallible function and check at every fallible call — to get the property "the failure modes a function participates in are visible from its signature." That's a property both humans and agents can rely on.

Why Not Just Result<T, E>?

You could express the same thing with a choice — a Result<T, E> type with ok and err variants. Zero gives you that pattern too; it's a useful tool when failure is data you want to inspect, store, or pass around.

What raises/check adds is a syntax-level convention for the common case: "if this fails, fail my function the same way." Without it, every call would be wrapped in a match that almost always re-packages the error into the caller's own Result. check is the shortcut for that, with the compiler ensuring the propagation is well-typed.

So:

  • Result<T, E> (a choice) — when you want to inspect or carry failure as a value.
  • raises + check — when you just want to propagate failure up the call stack.

Both are available; they cover different ergonomic needs.

Multiple Error Types

A function can raise more than one kind of error:

fun parse(input: String) -> i32 raises { Empty, Malformed } {
    if std.mem.len(input) == 0 {
        raise Empty
    }
    // ... parsing logic ...
    raise Malformed
}

A caller can:

  • Propagate with check parse(input) if its own signature lists both Empty and Malformed (or a superset).
  • Handle one or both errors explicitly with match or try-style constructs the language exposes for the purpose.

The exact syntax for fine-grained handling (matching on specific error types vs. propagating others) is one of the surfaces that may move in pre-1.0 Zero. The contract — every error type a function can raise is in its signature — is the stable bit.

A Mental Model

Zero's failure system is a strict-honest version of what every imperative language has:

ConceptTry/CatchZero
Mark a function as falliblenothing (implicit)raises { ... }
Raise an errorthrow eraise E
Propagate to callerbubbles invisiblycheck call(...)
Handle locallytry { ... } catch(e) { ... }match on a returned Result, or a typed handling form

The behavioral difference is small. The annotational difference is large — and on purpose. Effects are explicit. Failures are effects.

Style Notes

  • Use check liberally. It's the right default when you don't have anything specific to do at this layer.
  • Avoid bare raises on internal helpers. The narrower the error set, the more useful the signature.
  • Pair fallible operations with the capabilities they need. A World-using function that writes to stdout almost always wants raises because writes can fail.

Next: JSON Diagnostics

raises/check works alongside Zero's compiler diagnostics — when you write check against a function that doesn't declare the right errors, the compiler tells you exactly what's wrong in a structured form. The next doc covers JSON diagnostics — the machine-readable feed an agent reads to repair code.

Frequently Asked Questions

What does raises mean in Zero?

raises on a function signature declares that the function can fail. A bare raises allows any error type. A specific form like raises { InvalidInput } restricts the function to failing only with the listed error types. Callers must acknowledge the possibility of failure — either with check, or with another explicit handling form.

What does the check operator do?

check expr evaluates expr, and if it produces an error, propagates that error up to the current function's caller. Think of it as 'execute this, and if it fails, fail my caller with the same error.' For the caller's raises clause to permit the propagation, the caller must itself declare that it can raise a compatible error.

How do you raise an error in Zero?

Use raise ErrorName inside a function whose raises clause includes that error. Example: if ok == false { raise InvalidInput }. The function exits at that point; the error becomes the function's outcome, which the caller can check against.

Why doesn't Zero use try/catch?

Try/catch lets exceptions move silently through functions that aren't aware of them. Zero's design is that every function's signature has to acknowledge the failure modes it participates in. There's no hidden control flow — if a function can fail, its signature says so, and every caller has to acknowledge it explicitly with check.

Can a function raise multiple error types in Zero?

Yes — list them in the raises { ... } clause, separated by commas (or following the current Zero syntax for error sets). The set of possible errors is part of the function's contract, just like its parameter and return types. Callers can pattern-match on which error was raised or just propagate with check.

Coddy programming languages illustration

Learn to code with Coddy

GET STARTED