{"id":"KM-SW-0033","slug":"how-engineering-teams-should-think-about-error-handling","title":"Error Handling That Gives Teams a Safe Next Step","excerpt":"A practical error handling guide for engineering teams: classify failures by recovery, give each boundary a stable contract, protect diagnostics, and improve from evidence.","kind":"Tutorial","category":"software-engineering","tags":["error handling","Software Engineering","failure recovery","API errors","observability"],"seoKeywords":["error handling","failure recovery","API errors","error classification","observability"],"authorId":"krishnam-murarka","publishedAt":"2026-06-24","updatedAt":"2026-09-09","readingTime":"13 min read","image":"/social-images/blog/edilec-photo-km-sw-0033-1363e994333c.jpg","featured":false,"trending":false,"sourceCredits":[{"title":"Problem Details for HTTP APIs (RFC 9457)","url":"https://www.rfc-editor.org/rfc/rfc9457.html","author":"IETF"},{"title":"HTTP Semantics (RFC 9110)","url":"https://www.rfc-editor.org/rfc/rfc9110.html","author":"IETF"},{"title":"Error Handling Cheat Sheet","url":"https://cheatsheetseries.owasp.org/cheatsheets/Error_Handling_Cheat_Sheet.html","author":"OWASP Foundation"},{"title":"Recording Errors","url":"https://opentelemetry.io/docs/specs/semconv/general/recording-errors/","author":"OpenTelemetry"},{"title":"Node.js Errors","url":"https://nodejs.org/api/errors.html","author":"OpenJS Foundation"}],"researchSources":[{"title":"Problem Details for HTTP APIs (RFC 9457)","url":"https://www.rfc-editor.org/rfc/rfc9457.html","author":"IETF","reason":"Inspected the problem-detail fields and the distinction between client-correctable detail and implementation debugging information."},{"title":"HTTP Semantics (RFC 9110)","url":"https://www.rfc-editor.org/rfc/rfc9110.html","author":"IETF","reason":"Used to check the meaning of HTTP status classes and method semantics when discussing retry and response design."},{"title":"Error Handling Cheat Sheet","url":"https://cheatsheetseries.owasp.org/cheatsheets/Error_Handling_Cheat_Sheet.html","author":"OWASP Foundation","reason":"Checked security guidance on limiting technical disclosure and handling unhandled exceptions."},{"title":"Recording Errors","url":"https://opentelemetry.io/docs/specs/semconv/general/recording-errors/","author":"OpenTelemetry","reason":"Used the error-type and handled-versus-failed operation guidance to shape observability examples."},{"title":"Node.js Errors","url":"https://nodejs.org/api/errors.html","author":"OpenJS Foundation","reason":"Checked the runtime's error model and asynchronous error behavior before discussing process and request boundaries."}],"mediaAssets":[],"status":"published","body":[{"type":"paragraph","text":"Error handling is the design of what people and dependent systems can safely do when an intended operation cannot finish. A form validation error, an authorization denial, a slow payment provider, and a crashed worker may all become an exception in code, but they do not deserve the same message or recovery action. Engineering teams need a shared model that answers three questions: what outcome was attempted, what is known about its state, and who can act next. That model should be honest about uncertainty, protect sensitive diagnostics, and leave enough evidence for a later investigation."},{"type":"image","src":"/social-images/blog/edilec-photo-km-sw-0033-1363e994333c.jpg","alt":"Paper payment prototypes show a status-checking route that avoids duplicate submission.","caption":"A useful error state preserves work and gives the right actor a safe next step.","width":1200,"height":750},{"type":"heading","id":"error-outcome","text":"Begin with the decision a failure interrupts","depth":2},{"type":"paragraph","text":"Describe the interrupted business action before choosing an exception class. A user submitting an invalid tax identifier can correct the input. A caller without permission should receive a safe denial. A provider timeout after a payment request may require a pending state because the provider could have completed the charge. A nightly import with malformed rows needs a report and a bounded replay path. The same HTTP 500 or JavaScript Error object cannot express these different situations. Write the expected outcome, the uncertainty window, the safe action, and the owner of an unresolved case as part of the feature contract."},{"type":"heading","id":"error-actors","text":"Name who can act on each failure","depth":3},{"type":"paragraph","text":"A useful classification assigns the next action to a user, API client, operator, or system. User-correctable failures should identify the field or rule without erasing valid work. Client-retryable failures need a signal that the operation is safe to repeat. Operator-owned failures need a queue, evidence, and an escalation target. System-owned failures need bounded retries, a terminal state, and an alert that does not require someone to parse a stack trace. If no actor can say what to do next, the failure state is incomplete."},{"type":"table","columns":["Failure class","What the caller should learn","Safe next action"],"rows":[["Invalid input","Which value or rule needs correction","Edit and resubmit"],["Not authorized","The requested action is not available","Use an approved path or request access"],["Temporary dependency","Completion is delayed or being checked","Wait or retry under a stated limit"],["Conflict or duplicate","The current state differs from the request","Refresh, reconcile, or choose a state"],["Unexpected defect","The action did not complete","Use a safe support or recovery route"]]},{"type":"heading","id":"error-contracts","text":"Give each boundary a stable error contract","depth":2},{"type":"paragraph","text":"At an HTTP boundary, use the transport status for broad semantics and a stable application code or problem type for the condition a client can handle. [RFC 9457 Problem Details](https://www.rfc-editor.org/rfc/rfc9457.html) provides a standard shape for machine-readable errors; its detail text is intended to help a client correct a problem, not to expose debugging internals. [HTTP Semantics](https://www.rfc-editor.org/rfc/rfc9110.html) remains the authority for method and status meaning. Keep the public contract versioned and testable. A client should not have to parse a changing sentence to distinguish an invalid request from an unavailable dependency."},{"type":"heading","id":"error-detail","text":"Keep public detail useful and private detail protected","depth":3},{"type":"paragraph","text":"Show the implication and correction that a person can safely act on. Keep stack traces, query text, provider topology, account-existence clues, credentials, and unnecessary identifiers in protected telemetry. The [OWASP Error Handling Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Error_Handling_Cheat_Sheet.html) treats technical disclosure as a security concern because detailed errors help reconnaissance. Return a correlation reference that support can share without turning it into a bearer secret. The adjacent [REST API contracts guide](/blog/km-sw-0024/how-it-managers-should-think-about-rest-api-contracts/) is useful when the error response is itself a dependency for other teams."},{"type":"heading","id":"error-retry","text":"Make retry a property of the operation","depth":2},{"type":"paragraph","text":"Retrying is safe only when the operation and the failure class support it. A read after a transient network interruption may be retried with a limit. A payment or notification write needs idempotency, a durable request key, or a reconciliation check before a second attempt. If the client timed out after the provider accepted the request, report that the outcome is being checked rather than claiming a clean failure. Use exponential delay and a terminal state, and make the retry budget visible in metrics. A broad catch-and-retry wrapper is convenient at first but can amplify an outage or duplicate a side effect."},{"type":"table","columns":["Boundary","Unsafe default","Better recovery design"],"rows":[["Form","Clear every field and show a generic alert","Preserve valid values and identify the correction"],["API client","Retry every 4xx and 5xx","Use status, problem type, idempotency, and a limit"],["Worker","Retry forever on one poison item","Bound attempts and move to an owned terminal queue"],["Payment","Ask the user to submit again after timeout","Reconcile the provider outcome with a request key"],["Operator console","Expose the raw exception","Show safe context, correlation, and a repair action"]]},{"type":"heading","id":"error-telemetry","text":"Preserve investigation context with restraint","depth":2},{"type":"paragraph","text":"A diagnostic record should connect the operation, deployment version, dependency, timestamp, tenant-safe identifier, outcome, and correlation reference. It should not become a second customer database. [OpenTelemetry's recording-errors guidance](https://opentelemetry.io/docs/specs/semconv/general/recording-errors/) distinguishes an operation that truly failed from an error that was handled and allowed the operation to complete. Use a low-cardinality error type for grouping, record an exception once at the right boundary, and keep sensitive details out of broad spans and metrics. Define retention, access, redaction, and sampling with security and privacy owners."},{"type":"callout","tone":"tip","title":"Every retry needs a stopping point","text":"Name the operation, retryable classes, delay, maximum attempts, terminal owner, and customer-visible state. Without those decisions, a retry loop turns a visible failure into delayed and repeated damage."},{"type":"heading","id":"error-testing","text":"Test negative paths as first-class journeys","depth":2},{"type":"paragraph","text":"Write tests around outcomes, not only thrown values. Include malformed input, expired authority, a duplicate command, a slow dependency, a dependency that completes after the caller times out, a process restart during a write, a partial batch, and a denied operator action. Check the response, durable state, logs, metrics, alerts, and recovery instructions together. [Background job design](/blog/km-sw-0029/how-product-teams-should-think-about-background-jobs/) and [test strategy](/blog/km-sw-0030/how-it-managers-should-think-about-test-strategy/) provide useful adjacent patterns for asynchronous and failure-oriented tests."},{"type":"heading","id":"error-review","text":"Use error trends to change the system","depth":2},{"type":"paragraph","text":"Review errors by user outcome and recovery path, not only by exception name. Rising validation errors can indicate a confusing interface or a contract mismatch. A cluster of timeouts may indicate dependency capacity, a missing timeout, or a queue that should be asynchronous. Repeated manual replays can reveal an absent product state. Pair rates with a few real cases and segment by workflow, client, tenant, or release when the consequence differs. Turn a finding into a specific change: a new validation rule, a safer status, a bounded retry, a test, a dashboard, or a runbook owner."},{"type":"list","title":"Error handling review questions","items":["Can a user or caller tell whether the action completed, is pending, or did not start?","Does every public error have a stable code, type, or field context that clients can test?","Are retry and duplicate-effect rules defined for every consequential write?","Can support use a correlation reference without seeing secrets or unnecessary personal data?","Does telemetry distinguish handled business outcomes from failed operations?","Does each terminal error have an operator, a repair path, and a review signal?"]},{"type":"heading","id":"error-catalog","text":"Turn repeated failures into a maintained catalog","depth":2},{"type":"paragraph","text":"Pair the catalog with a small set of user-visible examples and operator cases. An example should show the input, public response, durable state, telemetry grouping, and safe next step. That makes review concrete and catches a common mismatch: a response says 'try again' while the server has already accepted the command. Keep examples versioned with the contract so they are exercised whenever a client or dependency changes."},{"type":"paragraph","text":"A useful error catalog is smaller than the exception hierarchy and richer than a list of messages. For each public type, record the business condition, status, safe detail, retry meaning, telemetry grouping, support route, and retirement owner. Include one example of the durable state that confirms the outcome. When a dependency, client, or product state changes, update the catalog and its tests together. This gives a new maintainer a way to preserve recovery semantics without guessing which details were accidental."},{"type":"paragraph","text":"Review the catalog against real cases each quarter or after a material incident. Remove categories that no longer describe a recovery path, merge types that have the same action, and split a type when callers need different behavior. A stable vocabulary makes dashboards and support guidance more useful, but it should remain accountable to the user outcome rather than becoming a taxonomy project."},{"type":"heading","id":"error-takeaways","text":"Error handling principles worth carrying into design","depth":2},{"type":"list","items":["Classify a failure by the next safe action, not by the exception class alone.","Use stable public contracts and keep implementation diagnostics private.","Make retry, idempotency, reconciliation, and terminal ownership explicit.","Record evidence that explains the operation without collecting secrets.","Turn recurring failure patterns into focused product, test, and operating changes."]},{"type":"heading","id":"error-faq","text":"Questions teams ask about error handling","depth":2},{"type":"heading","id":"error-faq-users","text":"Should users ever see a technical exception message?","depth":3},{"type":"paragraph","text":"Usually no. Show what the person needs to understand and do next, such as correcting a field, waiting for a pending result, or contacting support with a reference. Keep stack traces and internal topology in protected diagnostics."},{"type":"heading","id":"error-faq-retry","text":"When is retry safe?","depth":3},{"type":"paragraph","text":"Retry is safest when the failure is plausibly temporary and the operation is read-only, idempotent, or protected by a durable duplicate-effect check. It is not a universal remedy for validation errors, authorization denials, poison messages, or ambiguous writes."},{"type":"heading","id":"error-faq-status","text":"Are HTTP status codes enough for an API error?","depth":3},{"type":"paragraph","text":"They provide important transport semantics, but clients commonly need a stable problem type, field information, retry meaning, or a safe occurrence reference. Keep those additions consistent and documented rather than making clients parse prose."},{"type":"heading","id":"error-conclusion","text":"Conclusion: make failure a navigable state","depth":2},{"type":"paragraph","text":"Good error handling makes a difficult moment legible. Define the interrupted outcome, classify the recovery path, return a stable and safe contract, preserve restrained evidence, and review trends by consequence. When users, callers, operators, and systems can all see the next responsible action, failure stops being a dead end and becomes a managed part of the product."},{"type":"image","src":"/attachments/article-media/editorial/edilec-batch108-error-recovery-route.svg","alt":"Six-stage error handling route from interrupted outcome to tested improvement.","caption":"The route turns failure classification into a safe contract, protected evidence, and repair."}],"faqs":[{"question":"What is the first error handling decision an engineering team should make?","answer":"Name the business outcome that can fail, the actor who must act next, the uncertainty about completion, and the evidence needed to resolve it."},{"question":"How should an API expose detailed errors?","answer":"Use stable status and problem-type semantics with safe corrective detail, and keep stack traces, secrets, and infrastructure diagnostics in protected telemetry."},{"question":"What should be tested first?","answer":"Test invalid input, denied access, delayed and duplicated work, partial completion, process restart, and the recovery action a real operator will take."}],"relatedIds":["KM-SW-0024","KM-SW-0029","KM-SW-0030","KM-SEC-0045"],"relatedArticleIds":["KM-SW-0024","KM-SW-0029","KM-SW-0030","KM-SEC-0045"]}