Handling API Errors
Robust integrations treat errors as expected events and respond to each kind deliberately.
What you'll learn
- Treat errors as a normal part of integration
- Read a structured error response
- Separate retryable from non-retryable failures
- Surface useful messages without leaking detail
- Log enough context to debug later
6 min
Errors are normal
Things will go wrong, and a robust integration is built on accepting that from the start. Validation will fail, tokens will expire, networks will drop mid-request, and servers will occasionally falter under load. A resilient client does not treat any of this as a surprise — it expects each kind of failure and has a planned, deliberate response ready for it. The goal is graceful behaviour under imperfect conditions, not a fragile assumption of perfect ones.
Begin every reaction from the status code, which sorts failures into broad, meaningful classes, and only then read the response body for the specifics. Branching on the code first prevents one of the most common bugs in this area: confidently parsing a body that is not the shape you assumed, because the call failed in a way you did not anticipate.
Reading an error body
Well-designed APIs return a structured error document, almost always JSON, alongside the status code. A typical shape names the error, gives a human-readable message, and often includes a machine-readable code together with per-field details for validation problems, so you can pinpoint exactly what the server objected to.
{
"error": {
"code": "validation_failed",
"message": "amount must be greater than zero",
"field": "amount"
}
}In your own logic, read and branch on the machine-readable code rather than matching against the message text, which providers can reword at any time without warning and without considering it a breaking change. The message is written for humans to read; the code is the stable handle your software should depend on. Mixing those two roles up leads to brittle logic that breaks on a copy edit.
Retryable or not
Sort every failure by what you should actually do about it, because that decision matters more than the exact code. A 4xx generally means the request itself is wrong — bad input, missing authentication, no permission — and retrying it unchanged will fail in precisely the same way; the right move is to fix the cause first.
A 429 is a special case within that family: wait for the indicated delay, then retry the same request. A 5xx or a raw network failure, by contrast, is often transient, so retry it with exponential backoff while limiting automatic retries to safe or idempotent requests. Drawing this line clearly is exactly what separates a resilient client from one that either gives up far too soon or stubbornly hammers a failing endpoint into the ground.
Surfacing and logging
When you show an error to a user, translate it into something they can actually act on — a clear next step in plain language — and never expose raw internal detail or stack traces, which both confuse the user and leak information. Behind the scenes, by contrast, log generously: the status code, the error code, any request identifier the API returned, and a timestamp.
That request identifier is pure gold when you eventually need to contact the provider's support team, because it lets them locate your exact call in their own logs and skip a great deal of back-and-forth. Capture it whenever it is present in a response or header, rather than discarding it as noise — the one time you need it, you will be very glad it is there.
Putting it together
Centralise your error handling so that every single call benefits from the same battle-tested logic: check the status code, parse the structured body, decide whether the failure is retryable, log it with rich context, and surface a clean, actionable message to the user. Consistency in one place pays off across the entire integration and saves you implementing the same decisions repeatedly.
For the full set of status codes that drive these decisions — what each class means and how to react to it — read HTTP status codes, which is the foundation this error-handling approach is built directly on top of.
Key takeaways
- Expect errors and plan a response for each class
- Branch on the status code, then read the structured body
- Match on the machine-readable code, not the message text
- Retry 5xx and 429 with backoff; fix other 4xx at the source
- Log the request identifier and never leak internals to users
FAQ
Should I match on the error message or the code?
Match on the machine-readable code. Human-readable messages can be reworded at any time, which would silently break logic that depends on their exact text.
Which errors are safe to retry?
Generally 5xx and network failures, plus 429 after its Retry-After delay. Other 4xx errors need a fix, since retrying the same request will fail again.
Why capture the request identifier?
It lets support trace your exact call in the provider's logs, turning a vague report into a precise one. Log it whenever the API returns one.
Ready to build?
Read the API reference, grab the OpenAPI spec, and ship a resilient integration.