Handle third party API failure without misleading the user

A contact form stores a message, then calls an email provider. The provider times out. The API returns a generic failure, so the user submits again. Two messages now exist, and the original email may have been sent despite the timeout. Another implementation returns success after catching the provider error, even though the team will never receive the message. Both hide the actual state of the operation.
This example assumes a Node API, a local database, and an external delivery provider. The problem is the gap between durable local intent and remote completion. A database transaction cannot atomically commit a vendor HTTP request unless an explicit distributed protocol exists, which ordinary application integrations generally do not provide.
Define what the user requested
Decide whether success means the message was accepted for delivery or confirmed delivered to a provider. Those are different promises. A contact form can often acknowledge durable acceptance while a background worker handles delivery. A payment confirmation may require a stronger provider-state check.
Name states such as accepted, pending delivery, delivered, and failed with recovery available. Avoid a single boolean that means both record saved and provider succeeded. Store the operation identifier so a user retry or support investigation can refer to the same intent.
Define which failures users can correct. An invalid address may need an edit. A provider outage usually does not. The response should not tell users to resubmit a valid form when the system already accepted it and merely needs to finish delivery.
Trace the uncertain window
Write the sequence: validate, persist, call provider, receive response, update delivery state, respond to client. Consider a process crash or network failure after each step. If the provider accepted the request but the response was lost, a timeout cannot establish that nothing happened.
Check whether the provider supports idempotency keys, status queries, or delivery callbacks. Verify those APIs and their retention or retry semantics in the provider's primary documentation. Do not assume every provider treats a repeated request identifier the same way.
Ask an AI assistant to build the failure matrix from this exact sequence. Require it to identify unknown outcomes separately from confirmed failures. A suggestion to retry all exceptions can duplicate delivery, while a suggestion to suppress all exceptions can lose it.
Persist intent before delivery
For a contact message, store the message and a delivery job or outbox entry in the same local transaction when they must exist together. A worker claims pending entries, calls the provider, records the result, and retries according to the contract. Keep provider credentials out of the message record and public response.
A minimal operation record includes an identifier, owner or scope, validated payload reference, delivery state, attempt count, and safe provider correlation identifier. Avoid storing raw response bodies that may include sensitive information.
If a full outbox is unnecessary for a small feature, a durable pending field and a recovery process can still be more honest than a fire-and-forget promise. The essential requirement is that accepted work is not forgotten when the request process exits. The implementation should make unfinished work discoverable.
Make retries and claims safe
Use a scoped operation identity for client retries so the same form submission does not create another local record after a lost response. Idempotency keys explain that contract. The delivery worker also needs concurrency control so two instances do not process the same pending entry simultaneously without coordination.
A lease or atomic claim can bound worker ownership, but it does not eliminate ambiguity after an external call. If the lease expires while the first provider request is still in flight, another worker may act. Use provider idempotency or reconciliation where possible and state the remaining delivery guarantee honestly.
Retry transient failures with bounded backoff and an attempt policy. Do not repeatedly retry permanent validation errors. Preserve a terminal state and a manual recovery path when automatic attempts are exhausted. A queue that retries forever without visibility is not a recovery plan.
Return an accurate product state
After durable acceptance, the client can show that the message was received and delivery is pending. If immediate provider confirmation is required, show processing until that result is known. A success message should match the operation's durable state, not the browser's upload progress or the absence of a thrown exception.
If delivery fails later, the product may notify an administrator, display a recoverable status, or offer a user correction depending on the operation. Do not expose provider internals or credentials in that notification. Use the operation identifier for support.
For a request that fails before local persistence, a retry may safely create the operation. For a request with uncertain local outcome, look up its idempotency state before creating another record. The response should make these distinctions understandable without forcing users to reason about database transactions.
Verify partial success and recovery
Use a provider double to simulate rejection, timeout, acceptance followed by lost response, and success. Assert both local record state and user-visible response. A test asserting only the returned status can miss a saved record followed by a misleading failure.
Inject a process stop after local commit and before provider call, then run recovery. Inject a stop after provider acceptance and before delivery-state update. Verify the second case uses provider idempotency or the documented reconciliation procedure rather than blindly creating a duplicate.
Exercise at least one real provider integration in an isolated account when that boundary is part of the release. A double verifies application transitions; it cannot prove the vendor's actual idempotency, timeout, or callback behavior. Record the distinction in validation evidence.
Use AI review without sensitive payloads
Provide the state machine, worker claim logic, sanitized provider contract, and synthetic messages. Ask which transitions can be skipped by a crash and which states are recoverable. Ask whether any catch block returns delivered without evidence. These questions produce concrete findings.
Review generated retry code for unbounded loops, missing timeouts, and credentials in logs. Ensure callbacks authenticate the provider according to its documented mechanism and map to the correct operation. An external callback is untrusted input until verified.
Safe API auditing covers redaction, while transaction decisions covers local consistency. The repair is complete when the system can name what happened, preserve accepted intent, and recover the unfinished remote step without misleading the user.
Sources
About the Author

Kamruz
Full Stack Developer
A passionate developer who loves creating modern web applications and sharing knowledge with the community.
More About Me