Error handling becomes an engineering decision before the first endpoint, screen, queue, or migration exists. The useful question is not whether the team can adopt a familiar tool; it is whether the design will help people complete a payment attempt that can be declined, timed out, duplicated after retry, or accepted by the provider while the application loses the response with an outcome they can trust. That requires an explicit promise about authority, state, failure, and change. A polished prototype can hide those questions for a week or two. Production use cannot. This guide treats error handling as an operating boundary: a place where product intent, technical behavior, security, and support evidence must agree. The result is a build plan that can be challenged early, tested before release, and explained when a real exception arrives.
Define the error handling decision
Begin by writing one representative journey in plain language. For a payment attempt that can be declined, timed out, duplicated after retry, or accepted by the provider while the application loses the response, identify the initiating actor, the information they can rely on, the action they are allowed to take, the condition that completes the work, and who resolves disagreement. This is more useful than beginning with a vendor comparison because it creates a boundary for scope. The team should be able to say which record is authoritative at each stage and which behavior is visible to the person waiting for an answer. If the answer depends on another service, a scheduled task, or a human review, say so. Ambiguity at this point becomes a misleading status message, an undocumented manual step, or a costly support escalation later.
| Question | Decision to make | Evidence before build |
|---|---|---|
| User outcome | What proves a payment attempt that can be declined, timed out, duplicated after retry, or accepted by the provider while the application loses the response is complete? | A named actor, state transition, and acceptance rule. |
| Authority | Which record decides the current error handling result? | Owner, freshness expectation, and correction process. |
| Risk | What can go wrong without being immediately visible? | Failure class, safe fallback, and escalation owner. |
| Change | How will callers or users adapt to a changed rule? | Compatibility note, test fixture, and review date. |
Model the facts, not just the interface
The durable model for error handling should retain failure category, correlation identifier, safe user message, retry decision, owner, original request context, compensation action, and final resolution. Not every value needs the same permanence: temporary presentation state and safely recomputable views can remain outside the authority record. But an operator must be able to reconstruct why a consequential result was produced. Choose identifiers that survive retries and imports; record time in a way that distinguishes event time from processing time; and name the version of any rule or representation that can change. This protects the team from explaining an old decision with today’s code. It also gives testing a realistic target: fixtures can represent a business state, not a collection of anonymous fields.
RFC 9457 provides a standard structure for HTTP problem details. OWASP's error-handling guidance explains why safe disclosure matters; Node.js error handling and MDN's status-code reference keep implementation and protocol behavior connected. These references do not decide product policy for a team, but they are a strong check against inventing protocol or security semantics locally. Read them alongside the system’s actual consumers, data classification, and service-level commitments. A standard format is only helpful when ownership and behavior behind it remain clear.
Set boundaries and failure rules
The central boundary for error handling is expected validation feedback, recoverable dependency failures, security-sensitive failures, and defects that need an incident response. Make it visible in code and in the operating model. Name which component validates input, which owns the business decision, which persists the result, and which is allowed to communicate externally. Then design the uncomfortable cases: an expired permission, a stale record, a duplicate request, a delayed dependency, and a partial completion. Catching every exception at the edge and returning the same vague message without a correlation path, retry policy, or operator context is a common failure because it postpones a product decision until an incident. A safer design gives each failure an owner, a bounded response, and enough context for a person or process to continue safely.

| Failure condition | Design response | Signal to retain |
|---|---|---|
| Bad or incomplete input | Reject at the trusted boundary with a safe correction path. | Validation category and field or rule reference. |
| Duplicate or delayed work | Use a stable operation identity and inspect prior outcome. | Idempotency key, attempt history, and final state. |
| Dependency unavailable | Apply a deadline, bounded retry, or queued recovery path. | Dependency, elapsed time, retry decision, and owner. |
| Policy or authorization conflict | Deny safely and route a review where justified. | Principal, resource, policy version, and correlation ID. |
Deliver in small, observable increments
Build the smallest vertical slice that proves the decision. It should include a real boundary, a durable record, the expected unhappy path, and one way to inspect the outcome. Avoid a demo that bypasses identity, replaces a real dependency with an unrepresentative stub, or assumes a perfect sequence of events. Use representative fixtures, contract checks where another team depends on the result, and a controlled rollout for behavior that cannot be fully reproduced beforehand. The release plan should state who can halt the change, what evidence triggers rollback or containment, and how affected users will be told the current truth. That makes error handling a delivery capability rather than a single implementation milestone.
Operate from evidence
After launch, review failure rate by category, retries that later succeed, unresolved error age, duplicate side effects, alert quality, and support time to identify a request. A single aggregate dashboard is rarely enough: segment by journey, customer or tenant where appropriate, client version, dependency, and time window. Combine telemetry with sampled support cases, because a technically successful request can still produce a confusing human outcome. Write a lightweight review record for material changes: the intended behavior, observed evidence, decision owner, and follow-up. When a recurring exception appears, decide whether it is bad input, a missing product rule, a capacity constraint, or a broken contract. That discipline prevents permanent manual workarounds from becoming the hidden specification.
For error handling, stewardship is the discipline of joining a safe user message to an actionable operational trail. Preserve the failure category, correlation identifier, retry and compensation decisions, and resolution owner while limiting access to sensitive diagnostic detail. Review alerts against actual incidents: an alert that cannot tell someone what to inspect is noise. Over time, recurring categories should lead to a clearer validation rule, dependency contract, or recovery experience rather than another generic catch block.
Before finalizing error handling, write an error catalogue for one high-consequence journey. For each expected and unexpected failure, state what the user sees, whether the operation might have happened, whether retry is safe, which identifier joins the case to logs, and who owns resolution. Include a security-sensitive denial and a partial external success. Review the catalogue with support and on-call engineers. The result is a recovery model that prevents contradictory messages such as a failed screen followed by a completed charge or duplicated email.
Key takeaways
- Frame error handling around one real journey and a named user outcome.
- Store the facts needed to explain a consequential result after rules and code evolve.
- Make authority, authorization, retries, and exception ownership explicit at every boundary.
- Release a vertical slice with a meaningful failure path and a reversible operating decision.
- Use failure rate by category, retries that later succeed, unresolved error age, duplicate side effects, alert quality, and support time to identify a request to select the next improvement from evidence.
Frequently asked questions
What should an error response include?
Return a stable category or problem type, a safe human-facing explanation, a correlation identifier, and a next action when one exists. Avoid exposing internal stack traces, credentials, or authorization details. Operators can retain richer context in protected logs linked by the correlation identifier.
When is retry appropriate?
Retry only failures that are plausibly transient and whose operation is safe to repeat or protected by an idempotency key. Use bounded attempts, backoff, deadlines, and a visible terminal state. Retrying validation failures or uncertain side effects without an authority check can amplify the original problem.
Conclusion
Error handling is a promise that survives the first implementation. Make the promise narrow enough to test, explicit enough to operate, and durable enough to explain. Begin with the representative journey, record the facts and ownership that govern it, design the failure path before the launch path, and let observed outcomes refine the next version. Those choices give engineering teams something more useful than a technology decision: a system that can change without losing the trust of the people who depend on it.