Error handling for custom software should be designed with the same care as the main workflow. A bespoke product usually contains business rules, integrations, operator actions, and data transitions that a framework cannot infer. If a reservation, payment, document import, or approval step stops halfway, the system must say whether the result is refused, pending, partially applied, or unknown. The people supporting the product should be able to repair it without guessing which database row or provider call is authoritative. This guide treats error handling as a delivery capability: define the states, protect side effects, release a thin vertical slice, and improve the recovery path from evidence.

Define the promise your custom product makes
Start with a concrete customer or operator outcome. For an appointment product, the promise might be that a confirmed slot is visible to the patient and the practice. For a document workflow, it might be that an approved version is stored and distributed once. Write the normal path and the abnormal paths beside it: invalid input, missing authority, provider delay, duplicate submission, partial completion, and process restart. State which record is authoritative, who can correct it, and what the user sees while the outcome is uncertain. A custom product becomes safer when its behavior is explicit enough for a second team to operate.
Model incomplete, unknown, and refused work
Do not collapse every non-success into failed. Refused means the system made a definite decision not to apply the request. Incomplete means a known step remains. Unknown means a side effect may have occurred but the application lacks confirmation. Each state needs a customer message, an operator action, and a transition rule. A payment timeout should enter reconciliation, not invite an immediate second charge. An import with five bad rows should preserve the valid rows only if the product has deliberately chosen partial success; otherwise it should retain a report and leave the source unchanged.
| State | User or caller sees | System retains | Owner action |
|---|---|---|---|
| Refused | The request did not apply and why | Rule, actor, and safe reason | Correct input or authority |
| Incomplete | Work is still being processed | Step, attempt, and next check | Let the worker continue or intervene |
| Unknown | The outcome is being checked | Request key and provider reference | Reconcile before another write |
| Partial | Which items completed and which did not | Item-level results and source version | Repair or replay only the failed items |
| Defect | A safe support route | Correlation, release, and protected detail | Escalate, correct, and prevent recurrence |
Keep technical exceptions behind useful boundaries
Translate implementation failures at the boundary where a person or dependent service needs meaning. A database uniqueness error might become 'this invoice number already exists'; an upstream 429 might become a queued operation with a stated delay; a parsing exception might become a row-level correction report. Keep the translation close to the use case, not scattered across controllers and components. At the HTTP edge, follow RFC 9110 for status semantics and document the application-specific code. At the service edge, preserve a correlation reference but do not pass stack traces, credentials, SQL, or provider internals to a browser.
Write acceptance criteria for the hard path
For each important command, add acceptance criteria for invalid data, expired permission, duplicate request, dependency delay, process restart, and operator correction. State the observable record transition and the message at every step. A criterion such as 'shows an error' is too weak; 'shows pending, stores the request key, checks the provider, and prevents a second charge' can be tested and supported. Treat the error contract as part of the product surface, because a support agent or integration partner may depend on it for years.
Make recovery safe for side effects
Every recovery action should answer whether it can be repeated. Use a durable idempotency key for a command that creates a charge, sends a message, or changes an account. Stripe's idempotent-request guidance is a concrete example of storing the first result for a key so a retry does not perform the action twice. For an internal workflow, use the same principle even if the implementation is different: persist the command identity, validate that a repeat has the same intent, and return the known result or a conflict. When the side effect is ambiguous, reconcile first.
| Recovery action | Required guard | Evidence of safety |
|---|---|---|
| Retry a read | Bounded delay and timeout | Attempt count and final response |
| Replay a command | Idempotency or deduplication | Stable command key and prior result |
| Repair a record | Authorized operator and before-state | Reason, actor, and after-state |
| Reprocess a batch | Item-level result and source version | Reconciliation total and failed items |
| Reverse an effect | Compensating rule and approval | Linked reversal and customer communication |
Deliver failure behavior in thin vertical slices
Do not wait until the end of a custom build to decide how errors will be supported. Choose one real journey and take it through input validation, authorization, persistence, an external dependency, an operator view, telemetry, and a correction. Run it with a slow provider and a restart. The first slice should produce a useful record, a customer-safe status, and an alert or queue that another person can read. Then add the next journey using the same vocabulary only where the recovery semantics are genuinely shared. This prevents a generic error library from hiding product differences.
- Select one consequential journey and write its normal and uncertain outcomes.
- Define the public response, durable state, retry rule, and operator action together.
- Inject a dependency delay, duplicate request, malformed input, and restart.
- Verify idempotency or reconciliation before allowing a repeat of a side effect.
- Give support a safe status, correlation reference, and repair route.
- Review the first production cases before generalizing the abstraction.
Prepare support and engineering to read the same evidence
Support needs a compact view of state, customer impact, next check, and owner; engineering needs the protected detail that explains the cause. Design both views from the same event history. The OWASP Logging Cheat Sheet is a useful check on event selection, sensitive data, access, and resource limits. OpenTelemetry error handling also reinforces a valuable boundary: handlers should be scoped and expected exceptions should not be mistaken for unhandled process failures. Record enough context to reconstruct a decision without logging tokens, full documents, or personal data that nobody needs.
Give incidents roles and a repair sequence
A custom product should have a named incident lead, an investigator, a person responsible for customer communication, and an owner for the correction. Google's incident-management guidance emphasizes clear roles and a shared operational picture. In practice, the first question is often not why the exception happened but which customer records may be wrong. The runbook should point to the authoritative state, containment action, safe replay or repair, evidence to collect, and follow-up test. Keep the sequence short enough to use while attention is divided.
Measure error handling as product work
Track signals that reveal whether the product is becoming easier to recover: unresolved case age, time to determine state, retry success, duplicate effects prevented, manual repair rate, support contacts per failure, and recurring error classes. Establish a baseline before setting a target. A lower technical exception count can be misleading if the application now returns silent or ambiguous success. Review a sample of support cases, not just dashboards. When a pattern repeats, decide whether the right fix is a product state, a contract change, a validation rule, a test, a dependency policy, or an operator tool.
| Signal | Question it answers | Likely improvement |
|---|---|---|
| Unknown outcomes | How often must staff reconcile state? | Request keys, provider lookup, or clearer pending state |
| Repair age | Can the team restore correct data promptly? | Owned queue, runbook, or operator tool |
| Duplicate effects | Can the same command be repeated safely? | Idempotency and command history |
| Support contacts | Can users understand and recover? | Better message, help, or status visibility |
| Repeated exception class | Is a systemic defect being normalized? | Focused fix and regression test |
Keep a failure catalog close to the product
Record the states and recovery actions that are specific to the product rather than hiding them in a framework guide. A custom invoice flow may need separate categories for duplicate reference, provider pending, tax rejection, and operator correction even when all are represented by an exception internally. Keep the catalog beside acceptance criteria, support wording, dashboards, and runbooks. Review it when a business rule changes so an old error does not continue to promise a recovery path the product no longer supports.
The catalog also gives a team a safe way to discuss tradeoffs. If a manual repair is acceptable for a pilot, record its volume limit, response expectation, and expiry. If a workflow must reject partial batches, make that choice visible to the user and operator. These decisions keep custom software honest about what it can recover today while creating a clear backlog for the next investment.
Keep the surrounding delivery practices aligned with the recovery model. The REST API contracts guide helps when an external caller depends on error meaning; background jobs for custom software covers durable asynchronous work; and test strategy for custom software is useful when failure cases need to become repeatable evidence.
Custom software error-handling decisions to keep
- Define refused, incomplete, unknown, partial, and defective states before writing generic handlers.
- Treat side-effect recovery as a data and product decision, not a loop around an exception.
- Release one real journey with customer status, durable evidence, support ownership, and repair.
- Keep public messages useful and protected diagnostics available to the right investigators.
- Use production cases to decide which abstractions deserve to spread.
Questions teams ask about custom software error recovery
Where should a custom product start?
Choose one consequential customer journey and model its uncertain outcomes end to end. Include the durable state, public response, dependency behavior, operator evidence, and correction before applying the pattern to less important paths.
What should happen when a provider outcome is unknown?
Record a stable request identity, present a pending or checking state, and reconcile with the provider or authoritative store before allowing another side effect. Do not turn uncertainty into a confident failure message.
What proof is enough before expanding the pattern?
A second operator should be able to diagnose a real case, explain the visible state, and perform the approved repair without the original developer. The path should also pass duplicate, delay, restart, and access tests.
Conclusion: make recovery part of the product
Custom software earns trust when its difficult states are designed rather than improvised. Model uncertainty, protect side effects, keep public and private evidence distinct, and release recovery with the first real workflow. That discipline gives users an honest status and gives the operating team a way to restore the intended outcome.