وب مدرن، رابط کاربری و تجربهی وب · فصل ۵ — Error Model، REST، Streaming و Webhook · 48
Error Model: Domain, Public and Retryable Errors
Separate domain failure, public API representation and retry decision; map each boundary explicitly and prove one retry-safe and one retry-unsafe path with observable evidence. میتواند «Error Model: Domain/Public/Retryable Errors» را در یک feature محدود طراحی/پیادهسازی کند و correctness، accessibility یا failure behavior مرتبط را اثبات کند.
Lesson summary
Separate domain failure, public API representation and retry decision; map each boundary explicitly and prove one retry-safe and one retry-unsafe path with observable evidence.
میتواند «Error Model: Domain/Public/Retryable Errors» را در یک feature محدود طراحی/پیادهسازی کند و correctness، accessibility یا failure behavior مرتبط را اثبات کند.
This lesson produces
میتواند «Error Model: Domain/Public/Retryable Errors» را در یک feature محدود طراحی/پیادهسازی کند و correctness، accessibility یا failure behavior مرتبط را اثبات کند.
Who it is for
Foundation
Worked baseline, explicit evidence table, safe local fixture and one guided failure probe.
Repair
Reduce to one failing contract, observe evidence, repair, then restore full case.
Experienced
Skip syntax walkthrough; require independent implementation, mutation evidence and residual-risk statement.
Prerequisites
A 500 response is not a retry contract
A payment command times out after the downstream system may already have charged the card. The server does not know whether the side effect happened. If the API turns that uncertainty into a generic 500, exposes the internal exception, and the client retries every 5xx, one technical failure can become a duplicate business action. The opposite shortcut is also weak: collapse every failure into a vague public message and mark nothing retryable. That hides useful distinctions and forces clients to guess.
WEB-004 builds one bounded error model from three separate decisions: what failed in the domain, what the public HTTP contract is allowed to reveal, and whether a particular operation may be replayed now. Those decisions influence one another, but they are not the same data structure. The lesson ends with Error Boundary & Retryability Record v1, an evidence artifact that traces one retry-safe path, one retry-unsafe path, one deliberate failure mutation, one repair, and one changed-context decision.
The core warning is simple: status class is evidence, not a complete retry policy. Retry-After can communicate recovery timing, but it does not transform an ambiguous non-idempotent command into a safe replay. Likewise, Problem Details is a useful representation for many HTTP API errors, but it does not require the domain model to become an HTTP payload.
Scope, route, and evidence bar
This lesson assumes the HTTP/message basics from WEBF-009. It owns the error taxonomy, the internal-to-public mapping boundary, and a bounded retryability decision. It does not own Fastify architecture, a full idempotency-key protocol, pagination, streaming, webhook delivery, or a universal client retry library. When replay safety depends on an idempotency design, WEB-004 may name that requirement and refuse the retry; WEB-006 owns the deeper command/idempotency design.
Use the foundation route if domain failures, HTTP errors, and retry logic currently live in one catch block. Use the repair route if your API already has public error codes but clients still infer retry from 5xx. Use the experienced route if you can skip syntax and go directly to boundary review, ambiguous-outcome reasoning, and evidence quality. All routes finish with the same standard: observed request/response/client behavior, explicit inference, mutation/repair evidence, independent transfer, and a residual limitation.
The lab is local, deterministic, and non-destructive. It uses a fake downstream rather than a real payment provider. No browser or assistive-technology observation is required or claimed. Observable evidence is the API/client behavior produced by the fixture: status, public body, correlation identifier, retry decision, attempt count, and fake-side-effect count.
Mental model: failure source → domain meaning → public contract → HTTP semantics → retry decision
Treat the path as a sequence of boundaries rather than one exception handler.
- Failure source. Something concrete happens: invalid input, insufficient funds, dependency unavailable before a side effect, or an ambiguous timeout after a side effect might have occurred.
- Domain classification. Name the business/operational meaning without transport fields.
InsufficientFundsis not409;PaymentOutcomeUnknownis not503. - Public mapping. Decide what stable code and safe detail a caller may see. Preserve useful correlation evidence without leaking stack traces, SQL fragments, provider secrets, or raw exception objects.
- HTTP semantics. Choose a method/status/header combination consistent with the public meaning. HTTP semantics constrain the interface; they do not replace the domain taxonomy.
- Retry preconditions. Ask whether repetition preserves intended effect, whether the original outcome is known, whether a replay-safety contract exists, and whether the failure is expected to recover.
- Client decision. Retry automatically, surface a terminal error, or stop and reconcile an ambiguous outcome.
- Evidence and residual risk. Record what was observed, what was inferred, and what remains unknown.
This model exposes the most dangerous shortcut: jumping from “status is 503” directly to “retry is safe.” Recovery timing and replay safety answer different questions.
Foundation: five contracts that must remain separate
1. Domain-error contract — WEB-004-K1
A domain error represents meaning owned by the business or bounded application domain. It should be understandable without knowing whether the caller used HTTP, a job queue, a CLI, or an in-process function call. Examples in this lesson are ValidationFailure, InsufficientFunds, DependencyUnavailable, and PaymentOutcomeUnknown.
Keeping transport fields out of the domain type has two benefits. First, the same meaning can be mapped differently at another boundary without rewriting the domain model. Second, the handler cannot accidentally treat a transport convention as business truth. A domain error can carry a stable internal code and carefully bounded context, but it should not need status, Retry-After, or application/problem+json to exist.
This separation is not an argument for a giant inheritance tree. A small tagged value is enough when it preserves meaning. The important property is that domain classification happens before public serialization.
2. Public-error contract — WEB-004-K2
A public error is a compatibility surface. Clients may branch on a stable public code, show a safe message, attach a correlation identifier to support, or use documented recovery metadata. That makes public mapping an API-design decision, not repr(error).
A mapping should intentionally drop implementation details that are not part of the contract. A raw provider message such as an upstream host, stack frame, SQL text, or internal account identifier can be useful in protected logs while being inappropriate in the response. Redaction alone is not the whole security model, but the public contract must not depend on private internals.
Materially different failures should remain distinguishable when clients need different actions. Insufficient funds and a transient dependency outage are both “failed requests,” yet the first normally asks the user to change state while the second may be recoverable later. Collapsing both into operation_failed can erase the information needed for a safe client decision.
3. HTTP-semantics contract — WEB-004-K3
HTTP status and method semantics provide shared protocol meaning. They are inputs to the public design, not a substitute for the domain model. A 409, 422, 503, or 500 can communicate broad response semantics while a public error code explains the application-specific problem.
RFC 9110 defines safe and idempotent method properties and explains why idempotent requests can often be retried after certain communication failures. It also cautions against automatically retrying a non-idempotent request unless the client has an independent basis to know the request is effectively replay-safe or was never applied. That distinction matters more than the first digit of the status code.
The method alone is also not the whole answer. A badly designed GET endpoint that secretly performs an unsafe action violates method semantics. A POST can be made replay-safe by a separate application contract, but this lesson does not teach that protocol in full. We record the replay-safety precondition instead of inventing one.
4. Retryability contract — WEB-004-K4
Retryability is a conditional decision over at least four dimensions: operation semantics, failure class, outcome certainty, and recovery timing.
- Operation semantics: would repetition preserve the intended effect, or could it duplicate a command?
- Failure class: is the condition transient, terminal, validation-related, or ambiguous?
- Outcome certainty: do we know the original operation was not applied, or might the side effect already exist?
- Recovery timing: is there evidence that waiting could help, such as a documented backoff or
Retry-Aftervalue?
A retry-safe example is a read-like/idempotent operation that failed before a result could be returned due to a transient dependency outage. A retry-unsafe example is a payment-like POST that timed out after the provider might have applied the charge and no replay-safety/reconciliation contract exists. The latter should stop automatic replay and surface an “outcome unknown” state so the system can reconcile instead of guessing.
Retry-After answers “when might another request be appropriate?” It does not answer “is repeating this business operation safe?” The evidence record keeps those fields separate.
5. Problem-Details contract — WEB-004-K5
RFC 9457 defines a common machine-readable Problem Details representation for HTTP APIs. It is useful when an API needs a shared error shape with fields such as a problem type, title, status, detail, and instance, plus well-defined extensions. The standard also explicitly leaves room for existing domain-specific formats and does not require every error response to use application/problem+json.
Therefore the lesson supports two acceptable public representations in the fixture: a Problem Details-like response and a small domain envelope. Both must preserve the same public meaning and HTTP semantics. The representation choice does not make a failure retryable and must not redefine the semantics of the status code.
Problem type identifiers should be stable identifiers, not dynamic error messages. Human-readable detail can vary, but clients should not parse prose to decide whether to retry.
Deep explanation: ambiguity is a first-class state
The hardest failures are not always the ones with the most alarming exception. They are the ones that destroy knowledge about whether a side effect happened.
Consider a payment command. The client sends a request. The API calls a downstream provider. The provider accepts the command, but the connection drops before the API receives confirmation. From the API's perspective, two worlds are consistent with the observation: the charge happened, or it did not. Returning a generic 500 loses that distinction. Automatically retrying can duplicate the effect. Declaring permanent failure can also be wrong because the original action might have succeeded.
A better model promotes uncertainty to an explicit domain/public state such as payment_outcome_unknown. The public response can be a server-side failure class while the retry decision remains false. The next action is reconciliation or a product-specific recovery flow, not blind replay.
This is why “transient transport error” and “retryable business operation” must not be synonyms. The transport might recover in two seconds while the business operation remains unsafe to repeat.
Correlation IDs help evidence and operations without becoming public error meaning. The identifier should survive the mapping boundary so a client can report it and protected telemetry can find the corresponding event. It should not contain secrets, and its presence does not prove the response is secure or correct.
Worked example: payment-like command
The guided fixture models four outcomes.
Validation failure. The request is structurally unacceptable. The domain reports validation_failure; the public response is a client-error status with a stable code. Retry is false until the caller changes the request.
Insufficient funds. The command reached the business rule and failed deterministically. The public response preserves a stable insufficient_funds code without exposing the internal provider object. Automatic retry is false because time alone does not change the contract.
Dependency unavailable before effect. The fake downstream fails before any side effect. A read-like/idempotent request can be retryable after backoff. A side-effecting POST is not automatically replayed unless an independent replay-safety contract is declared. The fixture can expose Retry-After as timing evidence without treating it as proof of safety.
Ambiguous timeout after possible effect. The fake downstream increments its side-effect counter and then raises an ambiguous outcome. The public mapping uses payment_outcome_unknown. Automatic retry is false in the bounded fixture. The evidence is concrete: one side effect may already exist, and a second attempt would create a duplicate in the mutation.
The important observation is that two cases may both return a server-error status while producing different retry decisions. The status remains meaningful, but the client decision requires more evidence.
Failure modes and deliberate mutations
Mutation A — leak the raw domain/internal exception
Replace the public mapper with a response that serializes repr(error). Predict the failure: internal implementation detail crosses the public boundary and the response shape becomes coupled to private code. The repair restores stable public codes, safe detail, and correlation evidence while keeping protected diagnostics out of the body.
Mutation B — mark every 5xx retryable
Change the client policy to status >= 500. This should incorrectly replay the ambiguous payment command. The fake downstream side-effect count exposes the error. Repair by evaluating operation semantics and outcome certainty before retry.
Mutation C — let Retry-After prove replay safety
Change the policy so any response carrying Retry-After is automatically retried. The mutation must fail for a non-idempotent command whose outcome is unknown. Repair by treating the header as timing information only after replay safety is established.
Mutation D — collapse distinct public states
Map both insufficient_funds and dependency_unavailable to one generic code and identical client action. The public contract no longer preserves the distinction needed for recovery. Repair by keeping stable public categories that correspond to materially different caller behavior.
Experienced bridge: review the decision table, not the catch block
For an experienced engineer, the fastest path is a matrix review. List domain class, public code, status, sensitive fields removed, outcome certainty, replay-safety precondition, retry timing, client action, and residual uncertainty. If any row jumps directly from exception type to retry, the model is underspecified.
This also makes architecture discussions more precise. Teams can disagree about whether a particular failure deserves 409 or 422 while still agreeing that the domain error must not carry HTTP fields and an ambiguous side effect must not be auto-replayed. Separate the durable boundary from the negotiable representation choice.
Do not turn the matrix into a universal taxonomy for all services. Error models are product contracts. Reuse principles and vocabulary where they fit; keep public codes and recovery behavior bounded to the actual API.
Alternatives and trade-offs
Problem Details vs an existing domain envelope. Problem Details gives a standardized HTTP-facing structure and extension model. An established API might already have a compatible error envelope. Either can be acceptable when status semantics remain correct, stable machine-readable codes exist, private details are excluded, and clients do not infer retry from human prose.
Boolean retryable vs a richer recovery decision. A boolean is simple but can hide timing, ambiguity, maximum-attempt, or reconciliation needs. The lesson uses a boolean in the small fixture while recording the rationale separately. A production design may use richer recovery metadata, but that expansion must remain auditable.
Server-declared retry hint vs client-owned policy. The server often knows failure class and recovery timing; the client knows its operation context and replay contract. A robust design can combine both instead of treating either side as omniscient.
Aggressive automatic retry vs conservative reconciliation. Automatic retry improves resilience when replay is safe. Conservative stop-and-reconcile protects side effects when outcome is unknown. The correct choice depends on the operation contract, not on a preference for reliability or strictness in isolation.
Guided and independent practice
In the guided task, inspect the broken learner fixture before editing. Write one sentence for each boundary: domain meaning, public response, HTTP semantics, retry preconditions, and client action. Predict the four failure probes. Then implement the smallest repair that makes the observed behavior match the contract.
Record evidence as observation and inference separately. “Response status was 503 and attempts remained 1” is an observation. “Automatic replay was correctly suppressed because the outcome was ambiguous and no replay-safety contract existed” is an inference. Keeping them separate prevents a passing checker from becoming a substitute for reasoning.
For the independent transfer, apply the same method to a notification-delivery command. The external provider may accept the notification before the connection fails. Do not copy payment-specific codes. Define the domain meaning, public code, status, and retry decision for the new context. State whether reconciliation, a provider receipt, or a replay-safety contract would be needed before automatic retry could be justified.
Verification: what Error Boundary & Retryability Record v1 must prove
The record is complete only when it contains:
- the implemented domain/public mapping artifact;
- observed API/client behavior for at least one terminal case, one retry-safe case, and one retry-unsafe ambiguous case;
- the correlation identifier that survived the public boundary;
- one deliberate mutation prediction and actual result;
- the repair result;
- the independent changed-context transfer;
- an explicit statement separating observation from inference;
- at least one residual limitation.
A green validator proves only that the bounded fixture satisfies the encoded checks. It does not prove every status choice is ideal for every API, that a production payment integration is safe, or that security has been comprehensively reviewed. No browser/assistive-technology evidence is claimed by this API-only fixture.
Assessment and transfer
- Retrieval: state the five contracts and one non-goal without looking at the matrix.
- Application: repair the guided fixture and produce the evidence record.
- Diagnosis: predict and detect one deliberate mutation, then explain why it violates the contract.
- Decision: defend one mapping/retry choice against a plausible alternative, naming the evidence that changes the decision.
- Transfer: solve the notification-delivery ambiguity independently and state what remains unproven.
A strong answer does not merely repeat “idempotent” or “Problem Details.” It ties the term to the operation, failure class, public compatibility surface, and observed evidence.
References and freshness
RFC 9457 — Problem Details for HTTP APIs. Used for the bounded claim that Problem Details is a common machine-readable HTTP error format and does not replace every existing domain-specific representation. Rechecked 2026-09-13; review due 2026-10-17.
RFC 9110 — HTTP Semantics. Used for method safety/idempotency, status semantics, and the bounded retry claim that a non-idempotent request should not be automatically repeated without an independent basis for replay safety or proof that the original request was not applied. Rechecked 2026-09-13; review due 2026-10-17.
The stable method in this lesson is boundary separation and evidence discipline. Exact product mappings remain design decisions. If standards guidance changes, narrow the affected claims rather than silently expanding them.
Assessment
There is no answer key in this lesson pack; answer it yourself and mark honestly. "Needs review" sends the lesson to your review queue.
WEB-004-S1 retrieval
State the five WEB-004 contracts and one explicit non-goal.
WEB-004-S2 application
Repair the guided API fixture so domain classification, public mapping and retry decisions are separate and evidence-backed.
WEB-004-S3 diagnosis
Predict, run, diagnose and repair one mutation that would leak internals or duplicate a side-effecting command.
WEB-004-S4 decision
Defend one HTTP/public-error/retry decision against a plausible alternative using operation semantics and outcome certainty.
WEB-004-S5 transfer
Independently model an ambiguous notification-delivery outcome and state what must be known before automatic retry is justified.
Assessment rubric
Critical dimensions:
- Domain Error Boundary — Business/domain failures remain transport-agnostic; HTTP status, headers, correlation IDs and retry decisions are mapped outside the domain error object.
- Public Error Contract — Public codes/messages are stable, distinct where client decisions differ, preserve correlation, and do not expose internal implementation details.
- HTTP Semantics — Status/method semantics inform the mapping but are not treated as the complete domain model.
- Retryability Judgment — Retry depends on operation semantics, failure class, outcome certainty and an explicit replay-safety basis;
Retry-Afteralone is not replay-safety proof. - Problem Details & Transfer — RFC 9457 representation is used only where appropriate; the independent notification-delivery transfer preserves changed-context semantics, evidence coherence and one residual limitation.
Blocking assessments: S2, S3, S5. S1 and S4 are non-blocking but still require scope discipline. Machine checks do not fabricate browser/API evidence; evidence claims must match actually executed fixture behavior.
References & freshness
Case studies
Lab files
Repair the intentionally broken error model. Keep domain meaning independent from transport, map to a safe stable public contract, and make retry decisions from operation semantics + failure class + outcome certainty + recovery timing. Do not implement a full idempotency-key protocol. Complete the evidence record and the independent notification-delivery transfer.
Lesson media & slides
Teaching files (download)