0008. Separate expected outcomes, infrastructure failures and bugs
Questi contenuti non sono ancora disponibili nella tua lingua.
- Status: accepted
- Date: 2026-10-26
Context
Section titled “Context”The analysis (chapter 18) asks for three families of errors and forbids silent catch blocks. In TypeScript, thrown values are untyped, so expected business outcomes (“no availability for these dates”) are easy to forget at the call site, while wrapping every failure in a result type makes infrastructure code noisy.
Decision
Section titled “Decision”- Expected outcomes of a use case are returned, not thrown: a use case returns
Result<T, E>fromcore/shared, whereEis a union of plain error objects with a stablecode(for example{ code: "inventory.capacity_exceeded", date, available }). The compiler forces callers to handle them. The API maps each code to an RFC 9457 problem type with a stabletypeURI. - Infrastructure failures are thrown as
InfrastructureErrorsubclasses that state whether a retry may succeed (retryable), whether the operation may have taken effect anyway (outcomeUnknown, for a commit or a request whose answer was lost) and, when known, after how long. Adapters convert library errors into them at the boundary. The job runner decides on retries from these flags only. - Bugs (violated invariants, impossible states) throw plain errors through
assert-style helpers. They stop the operation, are logged with context and are never caught to continue.
A catch must rethrow, convert to one of the three families, or handle a documented case; Biome’s rules against empty blocks and a code review rule enforce it.
Consequences
Section titled “Consequences”- Use case signatures document their business failures, and the API’s error catalog is generated from the codes.
- Retry decisions are consistent across jobs, because every adapter reports retryability the same way.
- Developers must choose the family of each error; the rule of thumb is “could a correct program, with working infrastructure, hit this?” If yes, it is an expected outcome.
Alternatives considered
Section titled “Alternatives considered”- Exceptions for everything. Rejected: business outcomes would be invisible in signatures.
- Result types everywhere, including infrastructure (as in Effect or neverthrow). Rejected: heavy for adapters and unfamiliar to most contributors; it can be reconsidered if the codebase outgrows this split.
Amendment: 2026-10-29 — Channex adapters
Section titled “Amendment: 2026-10-29 — Channex adapters”The Channex adapters in packages/channex convert library errors at the boundary through one
helper, translateChannexFailures:
- Only a
ChannexErrorfrom the client is Channex’s failure. It becomes the port’s error with the client’sretryable,outcomeUnknownandretryAfterMs, and never with the upstream body, path or cause. - Anything else thrown during a call, such as a local check, a filter violation or a bug, passes unchanged and stops the operation.
- An ARI write returns a send outcome for every failure; one that is not Channex’s leaves the write’s outcome unknown.
- A connection check returns its expected outcomes:
request.invalidfor a refused key or property (401, 403, 404),server.unavailablefor any other Channex failure. - An ARI settings read returns its expected outcome,
channex.currency_unsupported, for a property currency GuestCraft cannot price in. The ARI pump pauses a property on a retryable failure; on any other failure, a bug included, or on that outcome, it stops the property until an administrator repairs it, resyncs it or reconnects Channex, and health shows why (ADR 0034). On 2026-10-30 this replaced a temporary exception that reported every ARI read failure as retryable, because the pump could not yet stop a property. - An administrator’s ARI mapping reads (rate-plan discovery, a mapping replacement) return their
expected outcome,
channex.mapping_read_refused(409), for a non-retryable Channex failure: a refused key or property, or an unusable answer. The use case converts it, so the API routes never inspect adapter errors; a retryable failure staysserver.unavailable(503) and a bugserver.internal(500).