Errors
One shape crosses every boundary: ArbeError. Both the throwable class — internals throw new ArbeError({ code, message, suggestion?, recoverable?, context? }) instead of bare Error — and the validated wire schema HTTP and CLI surfaces emit. code is dotted lowercase, namespaced by domain (task.already_claimed, validation.invalid_input, auth.forbidden, agent.dispatch_failed). Grow the enum by adding; never renumber or reuse.
HTTP { error: ArbeError.toJSON() } status from code map (no per-throw override)CLI stderr error: <message> → <suggestion>CLI --json { error: ArbeErrorPayload } identical to HTTP bodyJS client rejects with parsed ArbeError instanceHTTP status derives from code via a single map — if you need a new status for an existing code, add a new code instead. This rules out “record.not_found that returns 500” by construction. Boundary layers (SvelteKit +server handlers, worker routes, apps/cli/src/cli.ts) catch ArbeError, log the underlying cause, serialise. Unknown throws go through toArbeError() which wraps them as server.internal with the original as cause so crashes still render through the canonical path.
Internal helpers that never cross a boundary still throw native Error — cheap, idiomatic, won’t change. Code a consumer will see the failure of (CLI commands, HTTP endpoints, mutation executors) throws ArbeError. The line between “internal” and “boundary-facing” is the discriminator between developers-see-this-in-logs and users-see-this-on-their-screen — when in doubt, throw ArbeError; the cost is one import and one extra field.
We explicitly rejected the Result<Success|Failure> discriminated-union pattern. Throws propagate; only boundaries catch. Wrapping every function’s return type in a union is ceremony arbe’s code-style argues against (“optimistic execution — let errors throw”). ArbeError is the shape, not the protocol. When a new failure mode is genuinely a variant of an existing code, prefer reusing the code and putting distinguishing detail in message / context rather than minting a new code — codes are for switches that branch consumer behaviour; messages are for humans.
Code: @arbe/errors (ArbeError, ArbeErrorCodeSchema, toArbeError), re-exported via @arbe/core/schemas/arbe-error. Boundary catchers: SvelteKit +server handlers, worker routes, apps/cli/src/cli.ts.