Python Error Handling in APIs

In the last lesson you learned Parsing JSON Responses. So now you can read the data an API sends back. But here is the thing nobody tells you at the start. That call does not always succeed. The Wi-Fi drops. The server is busy. The website is down for a minute. If your code assumes the call always works, it crashes the moment the network has a bad day.

🤔 Why Error Handling Matters for APIs

When you call an API, your code talks to another computer somewhere on the internet. You do not control that computer. So a lot can go wrong that has nothing to do with your code.

Here is the pain. You write code that reads weather data and shows it on screen. It works fine on your machine all week. Then one morning the weather server is slow. Your call hangs. Your whole program freezes. Or the server returns an error page instead of data. Your code tries to read it as JSON. It crashes with a confusing message.

The fix is simple to say. Treat every API call as something that can fail, and plan for the failure. You check whether the response is actually OK before you trust it. You wrap the risky call so a failure does not crash everything. And you set a time limit so the program never waits forever.

Think of it like calling a friend on the phone. Sometimes they pick up. Sometimes the line is busy. Sometimes it just rings and rings. You do not stand there forever with the phone to your ear. You wait a bit. Then you hang up and try later. Good API code does the same thing.

🧱 The Things That Go Wrong

Before the code, let us name what actually fails. There are a few different kinds of trouble.

  • The request never connects. No internet, wrong address, or the server is completely down. The call cannot even start.
  • The request connects but takes too long. The server is alive but slow. Without a limit, your program just waits.
  • The request finishes but the answer is bad. You get a reply, but it is an error like “404 Not Found” or “500 Server Error”, not the data you wanted.

Each one needs a slightly different guard. We will handle all of them.

Note

This lesson uses the third-party requests library. If you do not have it yet, install it with pip. It is not part of the standard library.

Install it once from your terminal with this command.

Terminal window
pip install requests

🔎 Step One: Check the Status Code

Every HTTP response comes with a status code, a small number that says how the request went. 200 means “all good”. Anything in the 400 or 500 range means something went wrong.

This example calls an API and checks the number before trusting the data.

import requests
response = requests.get("https://api.github.com/users/octocat", timeout=10)
if response.status_code == 200:
data = response.json()
print(f"Name: {data['name']}")
print(f"Public repos: {data['public_repos']}")
else:
print(f"Something went wrong. Status code: {response.status_code}")

Here is what each part does.

  • requests.get(...) sends the request and waits for the reply.
  • response.status_code is that small number the server sent back.
  • We only call response.json() when the code is 200. So we never try to read data that is not there.

Output

Name: The Octocat
Public repos: 8

Note

This is example output. The numbers come from a live server, so the repo count you see may differ. The shape of the output stays the same.

Some status codes are worth knowing by heart. Here are the ones you will meet most.

Code Meaning What it usually means for you
200 OK Success. The data is there.
401 Unauthorized Your API key is missing or wrong.
404 Not Found The address or item does not exist.
429 Too Many Requests You called too fast. Slow down.
500 Server Error The server broke. Not your fault.

✋ Step Two: Let raise_for_status Do the Checking

Writing an if for every call gets tiring fast. The requests library gives you a shortcut. It is response.raise_for_status(). If the status is a 400 or 500, it raises an error for you. If everything is fine, it does nothing and the code keeps going.

This example does the same job as before. But it lets raise_for_status() flag the bad cases.

import requests
response = requests.get("https://api.github.com/users/octocat", timeout=10)
response.raise_for_status() # raises an error on 4xx or 5xx
data = response.json()
print(f"Name: {data['name']}")

Now the catch. raise_for_status() raises an error. If you do not catch that error, your program still crashes. So this line is only half the story. The other half is wrapping it in try / except. That is exactly the exception handling idea you already know, now used for real network code.

🛡️ Step Three: Wrap the Call in try/except

This is where it all comes together. We put the risky call inside a try block. If anything goes wrong, we catch it in except and handle it calmly instead of crashing.

This example calls the API and handles every kind of failure we listed earlier.

import requests
def get_user(username):
url = f"https://api.github.com/users/{username}"
try:
response = requests.get(url, timeout=10)
response.raise_for_status()
data = response.json()
return data["name"]
except requests.exceptions.Timeout:
print("The request took too long. Try again later.")
except requests.exceptions.RequestException as error:
print(f"The request failed: {error}")
return None
name = get_user("octocat")
if name:
print(f"Got the name: {name}")

Let us walk through it line by line.

  • The whole call sits inside try. So a failure jumps straight to an except.
  • except requests.exceptions.Timeout catches the slow case on its own. So we can give a clear “too slow” message.
  • except requests.exceptions.RequestException is the wide net. It catches connection errors, bad status codes from raise_for_status(), and other network trouble.
  • We return None on failure. So the caller can check and react, instead of getting a crash.

Output

Got the name: The Octocat

Tip

requests.exceptions.RequestException is the parent of almost every error requests can raise. Catch it last as a safety net. Catch the specific ones like Timeout first when you want a special message for them.

Why does the order matter? Python checks the except blocks top to bottom and stops at the first match. A Timeout is a kind of RequestException. So if you put the wide net first, it would catch the timeout before your special message ever runs. Specific first, general last.

⏱️ Always Pass a timeout

Notice that every example above has timeout=10. That is not decoration. Without a timeout, a stuck server can make your program wait forever. There is no error and no way out except killing it by hand.

The number is in seconds. timeout=10 means “give up if there is no reply within ten seconds”. When that limit passes, requests raises a Timeout error. Your except block then catches it.

This tiny example shows what happens when a server is too slow to answer in time.

import requests
try:
# This address delays its reply on purpose, longer than our limit.
response = requests.get("https://httpbin.org/delay/10", timeout=3)
print(response.status_code)
except requests.exceptions.Timeout:
print("Gave up waiting after 3 seconds.")

Output

Gave up waiting after 3 seconds.

Caution

A request with no timeout can hang for minutes or even forever. Always pass one. Pick a number that fits the job, often somewhere between five and thirty seconds.

🔁 Retrying Gently

Sometimes a call fails for a moment and works fine if you just try again. A quick network blip. A server that was busy for a second. For these, a gentle retry helps.

The key word is gently. Do not hammer the server with calls back to back. Wait a little between tries. And give up after a few attempts so you do not loop forever.

This example tries the call a few times, with a short pause between each attempt.

import requests
import time
def fetch_with_retry(url, attempts=3):
for attempt in range(1, attempts + 1):
try:
response = requests.get(url, timeout=10)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as error:
print(f"Attempt {attempt} failed: {error}")
if attempt < attempts:
time.sleep(2) # wait before trying again
print("All attempts failed.")
return None
data = fetch_with_retry("https://api.github.com/users/octocat")

Here is the idea in points.

  • The for loop gives us a fixed number of tries. So it can never loop forever.
  • On a failure we print which attempt failed. Then time.sleep(2) pauses for two seconds before the next one.
  • We only sleep if there is another attempt left. So we do not wait after the final try.
  • If every attempt fails, we return None and let the caller decide what to do.

Output

Attempt 1 failed: 500 Server Error: Internal Server Error for url: ...
Attempt 2 failed: 500 Server Error: Internal Server Error for url: ...

Note

This is example output for the failing case. On a normal day the first attempt succeeds and you see no failure lines at all.

For serious projects, a smarter pattern is to wait a bit longer after each failure. Two seconds, then four, then eight. That spacing-out is called backoff, and the requests ecosystem has tools for it. For now, a simple fixed pause is plenty to understand the idea.

⚠️ Common Mistakes

A few traps catch almost everyone the first time. Watch for these.

  • Reading the data before checking it worked. Calling response.json() on an error page gives you a confusing crash.
# ❌ Avoid: trusting the reply blindly
response = requests.get(url)
data = response.json() # crashes if the reply was an error page
# ✅ Good: check first, then read
response = requests.get(url, timeout=10)
response.raise_for_status()
data = response.json()
  • Forgetting the timeout. A missing timeout is the top cause of programs that freeze with no error.
# ❌ Avoid: this can hang forever
response = requests.get(url)
# ✅ Good: always set a limit
response = requests.get(url, timeout=10)
  • Catching every error silently. A bare except: that does nothing hides the problem. So you never learn what broke.
# ❌ Avoid: swallowing the error with no message
try:
response = requests.get(url, timeout=10)
except:
pass
# ✅ Good: catch the real type and say something useful
try:
response = requests.get(url, timeout=10)
response.raise_for_status()
except requests.exceptions.RequestException as error:
print(f"Request failed: {error}")

✅ Best Practices

Carry these habits into every API call you write.

  • Always pass a timeout. Every single call, no exceptions.
  • Call response.raise_for_status() so bad status codes turn into errors you can catch.
  • Catch requests.exceptions.RequestException as your safety net. Catch specific errors like Timeout first when you want a special message.
  • Give the reader a clear, calm message on failure instead of a raw crash.
  • Retry gently for short blips, with a small pause and a hard limit on attempts.

🧩 What You’ve Learned

A quick recap of what you can now do with confidence.

  • ✅ Check response.status_code to see if a call succeeded before you trust the data.
  • ✅ Use response.raise_for_status() to turn bad status codes into catchable errors.
  • ✅ Wrap API calls in try / except to catch RequestException and Timeout.
  • ✅ Always pass a timeout so your program never waits forever.
  • ✅ Retry gently with a short pause and a limit on attempts.

Check Your Knowledge

Test what you learned. Pick an answer for each question, then click Check.

  1. 1

    Why should you always pass a timeout to requests.get?

    Why: A timeout sets a limit so a slow or stuck server cannot freeze your program indefinitely.

  2. 2

    What does response.raise_for_status() do?

    Why: raise_for_status() raises an exception only when the response is an error (4xx or 5xx), so you can catch it.

  3. 3

    Which exception is the wide safety net that catches almost every requests error?

    Why: RequestException is the parent of nearly all requests errors, so catching it covers connection errors, timeouts, and bad status codes.

  4. 4

    When catching both Timeout and RequestException, what order should they go in?

    Why: Python matches except blocks top to bottom, so the specific Timeout must come before the general RequestException or it will never run.

🚀 What’s Next?

You can now make API calls that keep working through a bad network day instead of crashing. Next we leave the internet for a bit and put Python to work on your own machine. We will automate the boring file jobs you would normally do by hand.

Automating Files

Share & Connect