Skip to content

Errors

An error is a named failure a protocol function can raise. It has a required message and, optionally, fields.

1
2
3
4
5
/// Raised when a message is sent to an unknown recipient.
error RecipientNotFound {
    message = f"no recipient named {self.name}"
    name: string
}

message

An f-string, f"...", with {dotted.path} placeholders that are substituted when the error is rendered: {self.name} reads the error's own name field. Write a literal brace in it by doubling it: {{ and }}.

A plain "..." string is printed exactly as written, braces included. Only an f-string interpolates, here as everywhere else in the language (a constant's value, a validator's messages). A plain message that looks like it has a placeholder, such as "no recipient named {self.name}", gets a validation::message-not-interpolated warning, and the editor offers to add the f.

message is expected on every error.

This is the text a generator renders when a raised error is printed — the Rust generator builds a Display from it, so {} on a RecipientNotFound produces no recipient named ada rather than a field dump. Write it as the sentence you would want to read in a log.

Fields

After message, an error takes fields exactly like a struct — typed, optionally optional, with defaults and docstrings. They carry the data the message placeholders read.

Raising one

A function declares the errors it can raise with ! ErrorName:

1
2
3
protocol Mail {
    function send(message: Message) -> bool ! RecipientNotFound;
}

A function that can fail in more than one way lists them, comma-separated:

1
2
3
protocol Mail {
    function send(message: Message) -> bool ! RecipientNotFound, QuotaExceeded;
}

This is a bare reference — binding values into the error's fields at the raise site is not part of the language yet.

The name has to resolve to an error declared in this schema or brought in by a use. Three mistakes are build errors rather than something you discover in generated code:

Mistake Diagnostic
the name resolves to nothing validation::unresolved-throw — suggests the closest declared error when it looks like a typo
the name is a struct, enum or other declaration validation::throw-not-an-error
the same error listed twice validation::duplicate-throw

Only an error can follow ! because only an error has a slot in the schema's error space — the ordinal a raised failure actually travels as — and only an error carries the message that renders it.

What a function that raises nothing can still do

Declaring no ! means the function cannot raise anything the schema describes. It does not mean the implementation cannot fail: a dead database is not a schema concept, and a provider can always report a failure outside the schema, which reaches the caller as a generic "provider failure" with no detail attached. Nothing about it crosses the wire, so it can never be mistaken for one of your declared errors — and there is nothing to declare for it.

So add a ! for the failures a caller should be able to handle, and leave the rest to that channel. See the Rust and TypeScript guides for how each one surfaces.