How to check errors from asynchronous Web Services calls
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
Asynchronous service calls make applications responsive, but they also make failures easier to miss. A request can fail because the network is down, because the server returned an error status, or because the response body itself describes a business-level failure.
Good error handling starts by separating those failure modes. If you only check whether the call completed, you will miss important cases where the transport succeeded but the operation still failed.
Check the Right Layer of Failure
For an async web service call, there are usually three layers to inspect:
- transport errors, such as DNS problems or timeouts
- protocol errors, such as HTTP 400 or 500 responses
- application errors, such as a JSON payload that says the operation was rejected
In JavaScript with fetch, only network-level failures reject the promise automatically. HTTP 500 does not throw by itself, so you must inspect response.ok or response.status.
That pattern is the baseline. First detect transport and HTTP failures, then validate the payload contract.
Attach Error Handling at the Point of Completion
If the API uses promises, you can handle errors with try and catch around await, or with .catch() on the promise chain. If it uses callbacks, make sure the callback exposes an error object and that the caller actually checks it.
For example, a Node.js-style callback is explicit:
The exact mechanism changes by language, but the rule is the same: the error signal must be consumed where the async operation completes.
Log Enough Context to Debug
An error message without request context is not very useful in production. At minimum, log:
- the endpoint or operation name
- a correlation or trace ID
- the request duration
- the HTTP status code if available
- a sanitized copy of the server error message
This matters because asynchronous systems fail out of order. A user may trigger five requests at once, and the logs need enough information to identify which one failed and why.
If your backend returns structured errors, normalize them before they spread through the application:
Normalized errors make retries, alerts, and user-facing messages much easier to manage.
Timeouts and Retries
A common mistake is to wait forever for an async response. Every outbound service call should have a timeout. Then decide whether the call is safe to retry. Reads are often retryable; payment operations usually are not unless the API explicitly supports idempotency.
If you add retries, log the attempt count and back off between attempts. Silent retry loops can hide outages and amplify traffic spikes.
Expose Useful Errors to Users
Internal logs should be detailed; user-facing errors should be safe and actionable. A UI should not display stack traces, SQL fragments, or raw upstream payloads. Instead, translate technical failures into messages such as "The service is temporarily unavailable" or "Your request could not be completed."
That translation layer is especially important in browser and mobile apps, where asynchronous failures are common and often intermittent.
Common Pitfalls
- Assuming a resolved promise means success. HTTP error responses can still resolve normally.
- Catching an error and then swallowing it without logging or rethrowing it.
- Treating all failures as retryable. Some operations are not safe to repeat.
- Returning raw backend error details to users, which can leak internal information.
Summary
- Check transport, HTTP, and application-level errors separately.
- In promise-based code, inspect
response.okand validate the response body. - Handle errors where the async work completes, not only where it starts.
- Add timeouts, structured logging, and careful retry rules.
- Keep user-facing error messages simple while logging richer diagnostic context internally.

