Replies
A call ends in one of three ways: the operation answers with an output, it answers with an error, or it fails with an exception. The first two are replies. The third is not.
§Output
What an operation’s implementation returns is its output. An operation can declare the shape of that output, as it declares its input:
operations:
reduce:
type: transition
input:
type: object
properties:
amount: { type: integer, minimum: 1 }
required: [amount]
output:
type: object
properties:
total: { type: integer } Input and output schemas together form the operation’s contract: what a caller may send and what it can expect back. The runtime holds both sides to it.
§An error is an answer
Some outcomes are refusals the business logic expects: the order is not pending, the amount exceeds the total, the balance is insufficient. An operation expresses such an outcome by returning an error whose message is its code:
export async function transition (input: Input, entry: State) {
if (input.amount > entry.total)
return new Error('AMOUNT_EXCEEDS_TOTAL')
entry.total -= input.amount
return { total: entry.total }
} The codes an operation may answer with are part of its contract:
operations:
reduce:
errors: [AMOUNT_EXCEEDS_TOTAL] A refusal is the operation working as designed. The runtime treats it as a successful exchange whose content happens to be “no”: nothing is committed, nothing is retried, and nothing is reported as a failure. An error with a code the operation did not declare is not an answer it is allowed to make.
What a caller needs besides the code travels as the error’s cause:
return new Error('LOCKED', { cause: { until: '2026-01-01' } }) The code and the cause are all of an error that reaches a caller. Nothing else on the object does, so an error is as much a contract as an output is.
The shape of an error changed several times before it settled on this one, and two principles decided it.
Userspace is independent: the runtime never makes a component import anything from it. An error is therefore the
Errorthe language already has — no base class to extend, no factory to call — and an operation that refuses is still code that runs without the runtime.The message is the code because a contract carries what a program acts on. A text written for a person belongs in a log, where it can be reworded freely; in a contract it would be something callers come to depend on.
§An exception is a failure
Anything thrown is a failure nobody chose: a defect in the code, or something the operation depends on being unavailable. Exceptions are not declared, because they are not part of what the operation means to say.
The runtime raises exceptions of its own for the same kind of situation: a request that does not fit the contract, an entity that does not exist, a concurrent change the operation was not allowed to retry.
§What a caller receives
A caller receives a reply as a value and can tell an error from an output:
const reply = await context.remote.orders.reduce({
query: { id: orderId },
input: { amount: 500 }
})
if (reply instanceof Error) {
if (reply.code === 'AMOUNT_EXCEEDS_TOTAL')
return new Error('REFUND_REJECTED')
return reply
} The operation returned new Error('AMOUNT_EXCEEDS_TOTAL'), with the code as the message. The
runtime delivers it to the caller as an error whose code is that message, beside the cause it was returned with. It is handled where it has meaning or passed on as the caller’s own
answer.
An exception is not a value: it is thrown at the point of the call. An operation that does not catch it fails in turn, and the failure reaches whoever is waiting at the start of the chain.
§Why the distinction matters
The two outcomes drive different behavior throughout the runtime, so the choice between returning and throwing is not stylistic.
| Error | Exception | |
|---|---|---|
| Meaning | An expected refusal. | An unexpected failure. |
| Expressed by | Returning new Error('CODE'). | Throwing. |
| Declared | In the operation’s errors. | Never. |
| For a caller | A value to inspect. | Thrown where the call was made. |
| Where nobody is waiting | The message is done with. | The message is tried again. |
The last row is where the difference becomes consequential. An event or a task has no caller to receive the answer, so a refusal simply ends the work, while a failure makes the runtime try again. A refusal expressed by throwing would be retried as though something were broken. Delivery describes that behavior, which is the runtime’s distributed exception handling.