Handling Errors and Edge Cases Gracefully
A real network call can fail in many ways: no connection, a timeout, a malformed response. We build proper loading and error feedback instead of a program that silently hangs or crashes.
Every way a network call can fail, handled on purpose.
1. Learning Objective
By the end of this session you will be able to:
- Set a timeout on a network request and handle it when it fires
- Catch requests-specific exceptions distinctly from generic ones
- Design a simple three-state loading/success/error model for a network operation
- Provide clear, actionable feedback for each of those three states
- Bring every error-handling technique from the whole curriculum together into one cohesive, real operation
2. Pre-Coding Quiz
Answer these before reading the concept explanation or writing any code. It is fine to get these wrong โ that is the point. You need 4/5 to proceed to the lab.
Why should a network request always specify a timeout, e.g. requests.get(url, timeout=5)?
timeout=5 guarantees the request either succeeds or fails within a bounded time, raising a catchable exception if it does not.What does requests.exceptions.ConnectionError typically indicate, as opposed to requests.exceptions.Timeout?
Why is a three-state model (loading, success, error) more useful for a network operation than a program that just silently waits and then either works or crashes?
A network call might fail from a connection error, a timeout, OR return a non-200 status code (Session 42). Should all three be handled with a single, generic except Exception?
How does this session's error handling relate to everything else built across the curriculum, particularly Sessions 11, 24, 29, and 41?
3. The Concept โ Robust Network Error Handling
Each distinct network failure mode gets its own specific handling and message, per Session 11's catch-narrowly discipline.
Always set a timeout
Without a timeout, a request to an unresponsive server can hang the entire program indefinitely. A timeout guarantees the request either succeeds or fails within a bounded time.
import requests
try:
response = requests.get(
"https://www.apicountries.com/countries",
timeout=5, # give up after 5 seconds
)
except requests.exceptions.Timeout:
print("The request took too long and was cancelled.")
Catching distinct network failure modes specifically
The requests library provides specific exception types for different failure modes โ catching them individually (Session 11's discipline) gives clearer, more actionable feedback than one generic catch.
import requests
def fetch_countries_safely():
try:
response = requests.get(
"https://www.apicountries.com/countries",
timeout=5,
)
except requests.exceptions.ConnectionError:
return None, "Could not connect โ check your internet connection."
except requests.exceptions.Timeout:
return None, "The request took too long and was cancelled."
except requests.exceptions.RequestException as e:
return None, f"An unexpected network error occurred: {e}"
if response.status_code != 200:
return None, f"The API returned an error (status {response.status_code})."
return response.json(), None
A three-state model: loading, success, error
Rather than a silent pause followed by an unexplained result, explicitly represent and communicate what is currently happening โ directly analogous to the loading/error states covered in the original React course this curriculum is modeled on.
def load_countries_with_feedback():
print("Loading countries...") # the "loading" state, communicated explicitly
data, error = fetch_countries_safely()
if error:
print(f"Error: {error}") # the "error" state, with a specific, useful message
return []
print(f"Loaded {len(data)} countries successfully.") # the "success" state
return data
Bringing it all together
This combines Session 11's exception discipline, Session 30's validation, Session 41's resource safety, and Session 42's API adaptation into one cohesive, defensively-built operation โ the most robust piece of code in the entire project.
4. Lab
What you will build
A file called robust_loading_lab.py.
Step-by-step instructions
Create the file with a timeout-protected, specifically-handled fetch function
# robust_loading_lab.py
import requests
def fetch_countries_safely(url, timeout=5):
try:
response = requests.get(url, timeout=timeout)
except requests.exceptions.ConnectionError:
return None, "Could not connect โ check your internet connection."
except requests.exceptions.Timeout:
return None, "The request took too long and was cancelled."
except requests.exceptions.RequestException as e:
return None, f"An unexpected network error occurred: {e}"
if response.status_code != 200:
return None, f"The API returned an error (status {response.status_code})."
return response.json(), None
Test the happy path with the real API
data, error = fetch_countries_safely("https://www.apicountries.com/countries")
if error:
print("Error:", error)
else:
print(f"Loaded {len(data)} countries")
Test the error path with a deliberately bad URL
This should trigger ConnectionError handling, not a crash.
data, error = fetch_countries_safely("https://this-domain-does-not-exist-12345.example")
print("Error:", error)
print("Data:", data)
Test the error path with an intentionally short timeout
Use timeout=0.001 against the real API to force a Timeout exception.
data, error = fetch_countries_safely(
"https://www.apicountries.com/countries",
timeout=0.001,
)
print("Error:", error)
Combine everything into a full loading pipeline with explicit state messages
Adapt the data (Session 42), validate a sample (Session 30), and build a CountryRepository, all with explicit loading/success/error feedback.
from country_explorer import CountryRepository, validate_country_record
def adapt_api_record(raw):
return {
"name": raw.get("name", "Unknown"),
"region": raw.get("region", "Unknown"),
"population": raw.get("population", 0),
}
def load_country_repository(url):
print("Loading countries...")
raw_data, error = fetch_countries_safely(url)
if error:
print(f"Error: {error}")
return CountryRepository(raw_data=[])
adapted = [adapt_api_record(r) for r in raw_data]
if adapted:
validate_country_record(adapted[0])
print(f"Loaded {len(adapted)} countries successfully.")
return CountryRepository(raw_data=adapted)
repo = load_country_repository("https://www.apicountries.com/countries")
print(repo.get_all()[0].summary())
5. Expected Files Changed
| File | Action | Why |
|---|---|---|
robust_loading_lab.py |
Created | A fully robust, defensively-built network loading pipeline with three-state feedback. |
docs/sessions/session-43/index.html |
Created | This session document. |
6. Commit Checkpoint
Once the lab is complete and you can explain every line, make this exact commit:
git add robust_loading_lab.py docs/sessions/session-43/index.html
git commit -m "session-43: add timeouts, specific error handling, and loading/success/error feedback"
7. Code Review Checklist
Go through your code line by line and check each item:
8. Post-Coding Quiz
Same 5 questions. Take it again now that you have written and run the code. You need 4/5 to mark this session complete.
Why should a network request always specify a timeout, e.g. requests.get(url, timeout=5)?
timeout=5 guarantees the request either succeeds or fails within a bounded time, raising a catchable exception if it does not.What does requests.exceptions.ConnectionError typically indicate, as opposed to requests.exceptions.Timeout?
Why is a three-state model (loading, success, error) more useful for a network operation than a program that just silently waits and then either works or crashes?
A network call might fail from a connection error, a timeout, OR return a non-200 status code (Session 42). Should all three be handled with a single, generic except Exception?
How does this session's error handling relate to everything else built across the curriculum, particularly Sessions 11, 24, 29, and 41?
9. Reflection Questions
Think through these after the post-quiz. No right answer โ they are for discussion.
- Why does this session catch requests.exceptions.RequestException as a broader fallback AFTER the two more specific exceptions, rather than instead of them? What does exception ordering have to do with this?
- Trace through everything that would go wrong if fetch_countries_safely had no timeout at all and the API server happened to hang without ever responding.
- How many distinct sessions' worth of error-handling technique can you identify being used together in load_country_repository? List them.
- If you were building a real, user-facing application (not a console script) around this loading pipeline, what would the loading/success/error states actually look like to the user?
10. What Breaks If This Knowledge Is Missing?
- A frozen, unresponsive program: Without a timeout, one unresponsive server could freeze the entire program indefinitely โ completely unacceptable for anything meant to be used interactively or run unattended.
- Confusing, generic failures: Catching everything with one generic except Exception (rather than the specific types this session teaches) means a user experiencing a connection problem sees the exact same unhelpful message as someone hitting a slow timeout or a server error โ making the problem much harder to diagnose or explain to someone else.
- The capstone review (Session 44): This session's robust loading pipeline is the single piece of code most representative of everything the curriculum has built toward โ the final session reviews it, and everything else, end to end.
11. What We Learned
Python concept mastered: Robust network error handling โ timeouts, specific exception types for distinct failure modes, and explicit loading/success/error state communication.
Unlocks: The Country Explorer can now handle real-world network unreliability gracefully, giving clear feedback instead of hanging or crashing โ the last new skill before the capstone review.
Next session: Session 44 โ Capstone Review. The capstone. We walk through the entire project end to end, review every architectural decision, and take a comprehensive quiz across all seven layers.