Skip to content

    Reference04

    Errors

    Every error Bounda throws, its stable code, who sees it and whether a reaction retries it.

    Every error Bounda throws extends BoundaError, exported from @bounda-dev/core, and carries a code that stays the same across versions. Branch on code, or on the class where it is exported; the message is for people and can change.

    TypeScript
    import { BoundaError } from "@bounda-dev/core";
    try {
    await app.commands.placeOrder(payload);
    } catch (error) {
    if (error instanceof BoundaError && error.code === "VALIDATION_FAILED") {
    // show the issues
    }
    throw error;
    }

    Terminal means a policy, process or scheduled command that fails with it becomes a dead letter at once, with errorType: "terminal", since it would fail the same way again. Anything else is retried by the retry settings and, when they run out, becomes a dead letter with errorType: "retriable_exhausted".

    • DOMAIN_ERROR (DomainError; terminal). A command handler rejected with reject(code). rejected holds the code. app.commands throws it; a reaction gets it as a value instead.
    • FOREIGN_REJECTION (no exported class; terminal). A command handler let through a DomainError it did not make with its own reject, such as one rethrown from another app’s command. The command fails rather than rejects, so app.commands and a reaction both get this error, never the other command’s code. cause is that DomainError.
    • VALIDATION_FAILED (ValidationError; terminal). A payload, a duration, a returned process state or the after and limit of a read of the global stream did not validate. issues lists each problem with its path.
    • CONCURRENCY_CONFLICT (ConcurrencyError; retried). An append found the stream at another version, after runtime.commands.concurrencyRetries runs of the command. streamId, expectedVersion, actualVersion.
    • HANDLER_TIMEOUT (no exported class; retried). A command, policy or process handler ran out of time (runtime.commands.timeout, runtime.policies.timeout, runtime.processes.handlerTimeout).
    • REACTION_ABANDONED (no exported class; the run already failed). A command a reaction dispatched after its run was abandoned, or that was still running then. cause is why the run was abandoned.
    • REACTION_FINISHED (no exported class; the run already ended). A command a reaction dispatched after its run had finished, from a timer or a promise it left behind. Dispatch while the handler runs.
    • CHAIN_DEPTH_EXCEEDED (ChainDepthExceededError; terminal). A chain of reactions went deeper than runtime.policies.maxChainDepth. depth, maxDepth.
    • CREATION_ORDER (CreationOrderError; terminal). A command returned events that do not fit whether its aggregate exists: the first event is not one that exports begin, or an event that only exports begin goes on an aggregate that exists. Nothing is stored.
    • NOT_FOUND (NotFoundError; terminal). An unknown command or query, or a requested row or registry entry that does not exist.
    • INVALID_CONFIGURATION (ConfigurationError; terminal). bounda.config.ts or the registry is malformed: an unknown key, a port with no implementation chosen, a module missing an export. Thrown at boot, or when a handler reads a port a test left out.
    • CLAIM_LOST (ClaimLostError; handled by the runtime). A reaction’s inbox claim expired and another instance took it over. The attempt writes nothing; the other instance’s run is the one that counts.
    • SCHEDULED_CLAIM_LOST (ScheduledClaimLostError; handled by the runtime). The same for a scheduled command, a delayed policy run or a deadline, or the command was cancelled.
    • DEAD_LETTER_SETTLED (DeadLetterSettledError; an operator’s answer). A dead letter was retried or discarded after another retry or discard had settled it.
    • DEAD_LETTER_NOT_RETRIABLE (DeadLetterNotRetriableError; an operator’s answer). A dead letter cannot be retried in the app as it is now: its handler is gone or no longer handles the event, or its process failed on another step first. The letter stays failed.
    • REBUILD_SUPERSEDED (RebuildSupersededError; an operator’s answer). Another rebuild of the same read model took over; this one stopped without writing. readModel.

    The last three are answers to an operator, from app.deadLetters, bounda dead-letters and bounda rebuild, not failures of a handler.

    The hosts map the codes a client can cause to a status:

    Code createWorker (Cloudflare) failure() (React Router)
    VALIDATION_FAILED 400, with issues 400, with issues
    DOMAIN_ERROR 409, with rejected 409, with rejected
    CONCURRENCY_CONFLICT 409 rethrown
    CHAIN_DEPTH_EXCEEDED 409 rethrown
    FOREIGN_REJECTION 500, code INTERNAL, logged rethrown
    NOT_FOUND 404 rethrown
    HANDLER_TIMEOUT 504 rethrown
    anything else 500, code INTERNAL, logged rethrown, for the route’s ErrorBoundary

    createWorker answers with { error: { code, message } }, and 400 with INVALID_JSON for a body it cannot parse. failure() goes by the code too, so it answers the same whether the app runs in the process or in a Durable Object, whose refusals arrive as plain errors. See errors from the domain and the Cloudflare adapter.