Hello! Welcome back to our course.
In the previous lesson, we successfully made GET and POST requests using the requests library, allowing our program to communicate with web APIs. We learned how to send data, retrieve it, and inspect the server's response. However, our code so far has been optimistic, assuming everything will work perfectly.
This lesson directly addresses the learning outcome: Implement robust error handling for network operations using try-except blocks. Real-world networks are unreliable. Servers can be down, URLs can be incorrect, and network connections can be slow or intermittent. A robust agent must anticipate these issues and handle them gracefully instead of crashing. This lesson will give you the tools to build that resilience.
The Foundation: Python's try...except Blocks
Before we dive into network-specific errors, let's quickly review the fundamental error handling mechanism in Python: the try...except block. You might already be familiar with this, but it's crucial to have the flow clear in your mind.

The basic idea is:
- You place code that might cause an error (an "exception") inside the
tryblock. - If an exception occurs, the rest of the
tryblock is skipped, and Python jumps to theexceptblock that matches the type of error. - The code inside the
exceptblock is executed, allowing you to log the error, retry the operation, or exit gracefully.
Common Exceptions in the requests Library
While you could use a generic except Exception:, it's much better practice to catch specific exceptions. The requests library provides a hierarchy of custom exceptions that give you fine-grained control over error handling.
Let's explore the most common ones you'll encounter. This article provides clear, practical examples of each.
Exception Handling Of Python Requests Module
This article from GeeksforGeeks, titled 'Exception Handling Of Python Requests Module', is a great starting point. It breaks down the most frequent errors you'll face and shows how to catch them.
Please read through the first few sections of the article, which cover HTTPError, ReadTimeout, MissingSchema, and ConnectionError. Don't worry about the 'General Exception Handling' for now; we will cover how to structure multiple except blocks later in this lesson. Focus on understanding what kind of problem each exception represents.
To summarize the key exceptions:
requests.exceptions.HTTPError: Raised for unsuccessful status codes (4xx client errors and 5xx server errors). This is triggered by callingresponse.raise_for_status().requests.exceptions.ConnectionError: Raised for network-related problems, like a DNS failure or a refused connection. This happens when you can't even establish a connection with the server.requests.exceptions.Timeout: Raised when a request takes longer than the specifiedtimeoutvalue to complete.requests.exceptions.RequestException: This is the base exception from which all otherrequestsexceptions inherit. Catching this will catch any error originating from therequestslibrary.
Handling Specific Error Types
Let's build a robust request function by handling these errors one by one.
1. Handling Bad HTTP Status Codes (HTTPError)
In our last lesson, we briefly used response.raise_for_status(). This is the primary way to handle HTTP errors. It checks the response's status code and, if it's an error code (like 404 Not Found or 500 Internal Server Error), it raises an HTTPError.
import requests
# This URL does not exist, so it will return a 404 error.
url = "https://api.github.com/this-is-not-a-real-endpoint"
try:
response = requests.get(url)
print("Request was successful (this won't be printed).")
# This line will raise an HTTPError because the status code will be 404.
response.raise_for_status()
except requests.exceptions.HTTPError as err:
print(f"An HTTP error occurred: {err}")
# Sometimes the server provides useful error details in the response body
if err.response:
print(f"Response Body: {err.response.text}")
As you can see, catching the HTTPError allows us to inspect the error object and even print the response body from the server, which is invaluable for debugging. This technique is highlighted in a StackOverflow answer. In the answer by user "tsh", they show how to access e.response.text.
2. Handling Timeouts
Your agent can't afford to wait forever for an API to respond. The timeout parameter is essential for preventing your script from hanging. It can be a single number (for both connect and read timeouts) or a tuple (connect_timeout, read_timeout).
If the server doesn't respond within the specified time, requests raises a Timeout exception.
import requests
# Use a site that is known to be slow, or set an impossibly short timeout.
url = "https://httpbin.org/delay/5" # This endpoint waits 5 seconds before responding.
try:
print("Making a request with a 2-second timeout...")
# This request will fail because the timeout (2s) is less than the delay (5s).
response = requests.get(url, timeout=2)
except requests.exceptions.Timeout as err:
print(f"The request timed out: {err}")
For your agent, you could decide to retry a timed-out request a few times before giving up.
3. Handling Connection Errors
This error happens when you can't even talk to the server. This could be because your internet is down, a firewall is blocking the connection, or the domain name doesn't exist.
import requests
# This is not a real domain name.
url = "http://thissitedoesnotexistandneverwill.com"
try:
print(f"Attempting to connect to {url}...")
response = requests.get(url, timeout=5)
except requests.exceptions.ConnectionError as err:
print(f"A connection error occurred: {err}")
Structuring Your except Blocks: Specific to General
When you use multiple except blocks, Python checks them in order. It executes the first one that matches the exception. Because HTTPError, Timeout, and ConnectionError are all more specific types of RequestException, you must place them before the general RequestException block.
If you put the general except requests.exceptions.RequestException: first, it would catch all requests-related errors, and your more specific blocks for Timeout or HTTPError would never be reached.
The following resource provides an excellent, concise explanation of this ordering.
Correct way to try/except using Python requests module?
This StackOverflow answer clearly demonstrates why the order of your except clauses matters. It shows two code blocks with different orderings and the resulting output.
Focus on the two code blocks in this answer. Notice how in the first (correct) example, the specific HTTPError is caught. In the second example, the general RequestException catches it first, hiding the specific error type.
Putting It All Together: A Robust Request Function
Now, let's combine everything into a single, reliable structure. This is a pattern you can reuse throughout your agent's code whenever you need to make an API call.
import requests
def make_robust_request(url):
"""
Makes a GET request to a URL with robust error handling.
Returns the response JSON if successful, otherwise None.
"""
try:
response = requests.get(url, timeout=10)
# Raise an exception for bad status codes (4xx or 5xx)
response.raise_for_status()
# If we get here, the request was successful
print("Request successful!")
return response.json()
except requests.exceptions.HTTPError as errh:
print(f"Http Error: {errh}")
except requests.exceptions.ConnectionError as errc:
print(f"Error Connecting: {errc}")
except requests.exceptions.Timeout as errt:
print(f"Timeout Error: {errt}")
except requests.exceptions.RequestException as err:
# This is the most general exception, catching anything else
print(f"An unknown error occurred: {err}")
return None # Return None if any error occurred
# --- Let's test our function ---
print("--- Test 1: Successful Request ---")
make_robust_request("https://jsonplaceholder.typicode.com/todos/1")
print("\n--- Test 2: HTTP Error (404) ---")
make_robust_request("https://jsonplaceholder.typicode.com/todos/9999")
print("\n--- Test 3: Connection Error ---")
make_robust_request("http://thisisnotarealurl.xyz")
print("\n--- Test 4: Timeout Error ---")
# Using a very short timeout to force the error
try:
requests.get("https://jsonplaceholder.typicode.com/todos/1", timeout=0.001)
except requests.exceptions.Timeout:
print("Timeout Error: Request timed out as expected.")
This structure ensures you can identify and react to specific problems while still having a safety net for any unexpected issues.
Test your understanding!
Your task is to modify the make_robust_request function from above to perform a POST request instead of a GET request.
- The function should accept
urlandpayload(a Python dictionary) as arguments. - It should make a
POSTrequest to the givenurlwith thepayloadas JSON. - Implement the same error handling structure: catch
HTTPError,ConnectionError,Timeout, and finally the generalRequestException. - If the request is successful (the
requests.postcall doesn't raise an exception andraise_for_status()passes), the function should print "Post created successfully!" and return the JSON response from the server. - If any error occurs, it should print an appropriate error message and return
None.
Test your function by calling it with the URL https://jsonplaceholder.typicode.com/posts and a sample payload dictionary.
Show answer
import requests
def make_robust_post_request(url, payload):
"""
Makes a POST request to a URL with a JSON payload and robust error handling.
Returns the response JSON if successful, otherwise None.
"""
try:
# Use requests.post and pass the payload to the 'json' parameter
response = requests.post(url, json=payload, timeout=10)
# Raise an exception for bad status codes (e.g., 400, 500)
response.raise_for_status()
# Check for a 201 Created status, which is the typical success for a POST
if response.status_code == 201:
print("Post created successfully!")
return response.json()
else:
print(f"Request was successful, but status was {response.status_code}")
return response.json()
except requests.exceptions.HTTPError as errh:
print(f"Http Error: {errh}")
except requests.exceptions.ConnectionError as errc:
print(f"Error Connecting: {errc}")
except requests.exceptions.Timeout as errt:
print(f"Timeout Error: {errt}")
except requests.exceptions.RequestException as err:
print(f"An unknown error occurred: {err}")
return None
# --- Testing the function ---
sample_payload = {
"title": "My Robust Post",
"body": "This was sent using a function with error handling.",
"userId": 1,
}
post_url = "https://jsonplaceholder.typicode.com/posts"
print("--- Testing robust POST request ---")
result = make_robust_post_request(post_url, sample_payload)
if result:
print("\n--- Server Response ---")
import json
print(json.dumps(result, indent=2))
Conclusion
Congratulations! You've moved beyond simple requests and have learned how to build resilient code that can withstand the unreliability of the internet. This is a non-negotiable skill for creating any application that communicates over a network, and it's especially critical for an autonomous agent that needs to operate without constant human supervision.
Here are the key takeaways:
- Always wrap network requests in
try...exceptblocks. - Catch specific exceptions from the
requestslibrary likeHTTPError,ConnectionError, andTimeoutto handle different failure scenarios appropriately. - Use
response.raise_for_status()to turn bad HTTP status codes intoHTTPErrorexceptions. - Always include a
timeouton your requests to prevent your program from hanging. - Order your
exceptblocks from most specific to most general to ensure correct error handling.
In our next lesson, we will move on to a new topic: Define and use Python classes to structure agent components. Now that we have these foundational skills—handling data, making requests, and managing errors—we need a way to organize this logic into clean, reusable, and maintainable building blocks. Classes are the perfect tool for this, and they will form the skeleton of our AI agent.