Two APIs, two network calls, two ways the function can die. You want a dict with both counts even if one API throws an exception. Which side of a try/except should the API call live on?
The try block holds the risky call, and the except block gives me a fallback value? So one read per try/except, so a failure on one call doesn't kill the other?
Exactly. Each API call gets its own try/except. When Gmail is up and Calendar is down, you still get a real email count and a -1 for events — a partial snapshot instead of no snapshot:
try:
emails = toolset.execute_action(Action.GMAIL_FETCH_EMAILS, {"max_results": max_each})
email_count = len(emails.get("messages", []))
except Exception:
email_count = -1Exception catches everything — isn't that too broad? I've read that catching bare Exception hides real bugs.
For API boundaries, broad is the right call — network, auth, rate limits, bad JSON all raise different classes and all mean the same thing to your function: the read failed. Narrower catches belong in logic, not in I/O. Full shape:
def safe_two_api_count(max_each: int) -> dict:
try:
emails = toolset.execute_action(Action.GMAIL_FETCH_EMAILS, {"max_results": max_each})
email_count = len(emails.get("messages", []))
except Exception:
email_count = -1
try:
events = toolset.execute_action(Action.GOOGLECALENDAR_FIND_EVENT, {"query": ""})
event_count = len(events.get("items", []))
except Exception:
event_count = -1
return {"emails": email_count, "events": event_count}Why -1 and not 0? Doesn't 0 mean "no emails" just as well?
0 is ambiguous — is the inbox empty, or did the read fail? -1 is impossible for a real count, so it's a sentinel the caller can test. Any value outside the valid range makes the failure mode explicit.
So in thirteen lines I have a function that reads two APIs, never crashes, and tells the caller exactly which half failed — that's production-grade?
That is the shape of every automation that runs on a schedule. Failure of one source never blocks the others, so the caller always gets an answer.
TL;DR: Each risky read lives in its own try/except so one failure doesn't poison the others.
execute_actionexcept Exception at the I/O boundary-1 signals "read failed", separate from a legitimate 0| Value | Meaning |
|---|---|
0 | Source returned, list is empty |
-1 | Source threw, count unknown |
None | Alternative sentinel; needs caller-side None-check |
-1 is cheap, type-safe with int, and impossible for a real API count.
Two APIs, two network calls, two ways the function can die. You want a dict with both counts even if one API throws an exception. Which side of a try/except should the API call live on?
The try block holds the risky call, and the except block gives me a fallback value? So one read per try/except, so a failure on one call doesn't kill the other?
Exactly. Each API call gets its own try/except. When Gmail is up and Calendar is down, you still get a real email count and a -1 for events — a partial snapshot instead of no snapshot:
try:
emails = toolset.execute_action(Action.GMAIL_FETCH_EMAILS, {"max_results": max_each})
email_count = len(emails.get("messages", []))
except Exception:
email_count = -1Exception catches everything — isn't that too broad? I've read that catching bare Exception hides real bugs.
For API boundaries, broad is the right call — network, auth, rate limits, bad JSON all raise different classes and all mean the same thing to your function: the read failed. Narrower catches belong in logic, not in I/O. Full shape:
def safe_two_api_count(max_each: int) -> dict:
try:
emails = toolset.execute_action(Action.GMAIL_FETCH_EMAILS, {"max_results": max_each})
email_count = len(emails.get("messages", []))
except Exception:
email_count = -1
try:
events = toolset.execute_action(Action.GOOGLECALENDAR_FIND_EVENT, {"query": ""})
event_count = len(events.get("items", []))
except Exception:
event_count = -1
return {"emails": email_count, "events": event_count}Why -1 and not 0? Doesn't 0 mean "no emails" just as well?
0 is ambiguous — is the inbox empty, or did the read fail? -1 is impossible for a real count, so it's a sentinel the caller can test. Any value outside the valid range makes the failure mode explicit.
So in thirteen lines I have a function that reads two APIs, never crashes, and tells the caller exactly which half failed — that's production-grade?
That is the shape of every automation that runs on a schedule. Failure of one source never blocks the others, so the caller always gets an answer.
TL;DR: Each risky read lives in its own try/except so one failure doesn't poison the others.
execute_actionexcept Exception at the I/O boundary-1 signals "read failed", separate from a legitimate 0| Value | Meaning |
|---|---|
0 | Source returned, list is empty |
-1 | Source threw, count unknown |
None | Alternative sentinel; needs caller-side None-check |
-1 is cheap, type-safe with int, and impossible for a real API count.
Create a free account to get started. Paid plans unlock all tracks.