Every API call can fail. The network can drop, a server can time out, and a key can expire or become invalid. That’s not necessarily a sign of a poorly built API. It’s simply how distributed systems work.
What separates a fragile integration from a reliable one isn’t whether errors happen. It’s how your code responds when they do. A good error-handling strategy has three parts: classify the error, retry the ones worth retrying, and have a fallback plan when retries run out.
This guide walks through that strategy step by step, using a geolocation API as the working example. The same pattern applies to almost any HTTP API you call from your app.
Key Takeaways
- Not every error deserves a retry. Client errors (4xx) usually mean fix the request, not try again.
- Exponential backoff with jitter spreads out retries so you don’t hammer a struggling server.
- GET requests, like IP lookups, are generally safe to retry because they don’t change server-side data.
- A graceful fallback, such as a cache, default value, or feature toggle, keeps your app usable when the API is unavailable.
- A small wrapper function around your API calls can handle all of this in one place, so you don’t repeat the logic throughout your code.
Let’s start with a one-line summary of the whole strategy before we go deep into each part.
Short Answer: Classify, Retry What Makes Sense, Always Have a Fallback
One of the key API error handling best practices is knowing when not to retry. First, figure out what kind of failure it is. A wrong access key will fail every time, so retrying only wastes time and quota. A dropped connection might succeed on the next attempt. Once you know the type of failure, retry only when it makes sense, and space out those retries so you don’t make the problem worse.
But retries are not a guarantee. Sometimes an API stays down longer than your app can wait. That’s where degradation comes in. Your app should have a plan B: show cached data, use a default value, or quietly turn off a feature until the service recovers. A user should never see a broken page just because one API call failed.
With that summary in place, let’s look at how to actually classify an error.
API Error Handling Best Practices: Classify Errors First
To handle API errors properly, start by putting each failure into one of three buckets: client error, server error, or network error. Each one needs a different response.
Client errors (4xx) usually mean don’t retry. These happen when something is wrong with your request, not the server. A missing access key, an invalid IP address, or a request to a plan-restricted feature all fall here. Retrying the exact same request will just fail again in the same way. Fix the request first.
For a geolocation lookup using an endpoint like https://api.ipstack.com/{ip_address}, two common client errors are:
- Code 101, invalid_access_key: The API key is missing, wrong, or has been reset. No amount of retrying fixes this. Check the key and alert a human if it’s a production issue.
- Code 104, usage_limit_reached: The monthly quota is used up. Retrying won’t help. The real fix is to wait for the quota to reset, queue the request for later, or move to a higher plan.
Server errors (5xx) deserve a maybe. A 500 or 503 usually means something went wrong on the server side, and it might be temporary. A brief retry with backoff is often worth it. But if the server keeps failing after a few attempts, stop and fall back. Repeated retries against a struggling server only add more load to it.
Network errors deserve a yes, with a caveat. Timeouts, DNS failures, and dropped connections are often temporary and can be good candidates for retrying. But a timeout doesn’t guarantee that the server never received the request, so retries are safest for operations that are idempotent.
Here’s how that breaks down in one view:
Once you know what to retry, the next question is how to space out those retries. That’s where a good retry strategy comes in. Exponential backoff is a simple place to start.
Exponential Backoff with Jitter, Explained Without Math Anxiety
Say a request fails and you decide to retry. Retrying immediately is tempting, but it’s usually a bad idea. If the server is struggling under load, an instant retry only adds to that load. Worse, if many clients are retrying at the same time, they can create a traffic spike of their own.
Exponential backoff fixes this by waiting longer between each retry. The wait time roughly doubles with each attempt:
- Attempt 1 fails, wait 1 second
- Attempt 2 fails, wait 2 seconds
- Attempt 3 fails, wait 4 seconds
- Attempt 4 fails, wait 8 seconds
This gives the server some room to recover instead of hitting it again immediately.
Jitter is the small extra piece. Instead of waiting exactly 4 seconds, you add a small random delay to the backoff time. Why bother? Picture a hundred clients that all fail at the same moment. Without jitter, they could all retry at roughly the same time, creating another traffic spike. With jitter, those retries spread out more naturally.
You don’t need to overthink the math here. A simple rule works well: increase the wait between attempts, add some randomness, and cap the delay at a reasonable maximum, such as 30 seconds. That prevents a long chain of failures from leaving a user waiting indefinitely.
Backoff only makes sense for retryable errors, though. Before retrying anything, it helps to understand why GET requests are generally safe to retry in the first place.
Idempotency: Why GET Lookups Are Generally Safe to Retry
An idempotent request is one where sending the same request multiple times has the same intended effect as sending it once. A GET request, like an IP lookup, is a good example. Asking “what’s the location data for this IP?” ten times doesn’t change the resource on the server. It simply returns the requested data each time, network hiccups aside.
This matters because automatic retries are safest when repeating a request doesn’t create an unwanted side effect. POST requests that create a new record, charge a payment, or send an email aren’t necessarily safe to retry, since a retry could duplicate the action.
Before adding automatic retries to any API call in your app, check whether the operation is idempotent. If it is, like a typical read-only lookup, retrying is generally low-risk. If it isn’t, you need extra protection, such as an idempotency key, before retrying it.
For read-only lookups, retries are usually straightforward. But what happens when you’ve exhausted your retries and the API still isn’t responding? That’s where your fallback plan matters.
Graceful Degradation Patterns: Cached Fallback, Feature-Off, Default Value
Retries buy you time, not guarantees. At some point, you have to accept that the call failed and decide what your app should show the user instead of an error screen.
A few patterns work well here:
Cached fallback. If you looked up the same IP or resource recently, serve the cached result instead of failing outright. Geolocation data for a given IP doesn’t usually change quickly, so a cache that’s a few hours or even a day old may be acceptable for many use cases. The right freshness window depends on how accurate the feature needs to be.
Feature-off. If the failing call powers a non-critical feature, such as showing a visitor’s country flag next to their comment, hide that feature for this request instead of blocking the whole page. The user still gets everything else.
Default value. If an approximate answer is good enough, fall back to a sensible default. For location data, that might mean showing “Unknown region” instead of leaving the field blank, or using a general currency and language instead of a localized one.
Pick the pattern that fits the feature. A checkout page might need a cached fallback or another carefully designed recovery path. A “Welcome, visitor” banner can happily fall back to a generic greeting.
Now let’s put all three pieces (classify, retry, degrade) into one small, reusable wrapper.
A Complete Resilient Client Wrapper
Instead of scattering this logic across your codebase, wrap it once. Here’s a compact example using an IPstack lookup. It classifies the response, avoids retrying errors that won’t resolve on their own, retries temporary failures with backoff, and calls a fallback when it gives up.
JavaScript:
async function resilientLookup(ip, accessKey, fallback, maxAttempts = 4) {
const url = `https://api.ipstack.com/ip?accesskey={accessKey}`;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
const res = await fetch(url);
const data = await res.json();
if (data.success === false) {
const code = data.error.code;
// These errors won't be fixed by retrying.
if (code === 101 || code === 104) {
return fallback(data.error);
}
return fallback(data.error);
}
return data;
} catch (error) {
// Network error, retry below.
}
if (attempt < maxAttempts) {
const base = Math.pow(2, attempt) * 1000;
const jitter = Math.random() * base * 0.5;
await new Promise(resolve =>
setTimeout(resolve, base + jitter)
);
}
}
return fallback({ type: "max_attempts_reached" });
}
Python:
import time
import random
import requests
def resilient_lookup(ip, access_key, fallback, max_attempts=4):
url = f""
params = {"access_key": access_key}
for attempt in range(1, max_attempts + 1):
try:
res = requests.get(url, params=params, timeout=5)
data = res.json()
if data.get("success") is False:
code = data["error"]["code"]
# These errors won't be fixed by retrying.
if code in (101, 104):
return fallback(data["error"])
return fallback(data["error"])
return data
except requests.RequestException:
# Network error, retry below.
pass
if attempt < max_attempts:
base = 2 ** attempt
jitter = random.uniform(0, base * 0.5)
time.sleep(base + jitter)
return fallback({"type": "max_attempts_reached"})
Both versions follow the same pattern: make the request, classify the failure, retry when appropriate, and fall back when attempts are exhausted. The fallback function is where your cached value, default, or feature-off logic lives.
The example uses IPstack because a geolocation lookup is a straightforward way to see these patterns in practice. The same wrapper approach can be adapted to other HTTP APIs, with the specific errors and retry rules adjusted to match each API’s behaviour.
💡For the complete list of IPstack API error codes and what they mean, the IPstack API documentation is a useful reference.
Conclusion
API error handling best practices aren’t about preventing every failure. They’re about building a client that knows the difference between “try again” and “stop and adjust,” waits sensibly between retries, and always has a fallback so one failed call doesn’t take down the whole experience for your users.
Start small: classify the errors your app already sees, add backoff to the ones worth retrying, and choose one fallback pattern for your most important feature. That alone puts you ahead of an integration with no error handling at all.
If you’re working with IPstack, the IP API tutorial hub is a useful next step for setting up the API and understanding the error codes it can return. The same error-handling principles apply whether you’re building around IPstack or another HTTP API.
Frequently asked questions
Do I need to retry every failed API call?
No. Only retry errors that are likely to succeed on another attempt, such as timeouts, dropped connections, or temporary server errors. Retrying a client error, such as an invalid access key, usually just wastes time and quota.
How many retry attempts should I use?
Three to five attempts is a reasonable starting point for many applications. The right number depends on how long your users can wait and how important the request is. More retries can simply delay your fallback without adding much benefit.
What's the difference between backoff and jitter?
Backoff is the increasing wait time between retries. Jitter adds a small random delay to that wait, helping prevent multiple clients from retrying at the same time.
Is it safe to retry a POST request the same way as a GET request?
Not always. GET requests are generally safe to repeat because they’re intended to retrieve data without changing the resource. POST requests can create duplicates or trigger an action more than once, so check whether the operation is idempotent before retrying it.
What should my app show the user if the API stays down?
Whatever keeps the experience usable: a cached value, a sensible default, or a non-critical feature temporarily turned off. Avoid exposing raw API errors to the end user.
Try ipstack free
IP-to-location, ASN, ISP, time zone and threat data from one endpoint. Get a key and make your first call in under a minute.
IP Geolocation
Giving models real location data instead of a training cutoff — the ipstack MCP server, agent tool use, and how the current LLMs handle the API.
IP Geolocation
Giving models real location data instead of a training cutoff — the ipstack MCP server, agent tool use, and how the current LLMs handle the API.