Exception Handling
An agent talks to things it does not control — a model, an API, a database, a network. All of them fail sometimes. Exception handling is how your program survives that instead of stopping.
The basic shape
try:
result = 10 / 0
except ZeroDivisionError as e:
print("cannot divide by zero:", e)
finally:
print("this always runs")try— the code that might failexcept— what to do if it doesas e— the exception object, holding the messagefinally— runs either way, for cleanup
Without the try, the program stops at the failing line and nothing after it runs.
Catch what you expect, not everything
try:
data = json.loads(text)
except json.JSONDecodeError:
data = {}Naming the specific exception is the single most important habit on this page.
try:
result = call_api()
except Exception:
passThat is the pattern to avoid. It hides typos, hides bugs in your own code, and turns a clear failure into an agent that quietly returns nothing. When someone says "it just stops working and there is no error", this is usually why.
You can catch several, in order, most specific first:
try:
result = call_api()
except TimeoutError:
result = retry()
except ConnectionError:
result = None
except Exception:
logger.exception("unexpected failure")
raiseNote the last block: log it, then raise to let it carry on upwards. Catching in order to log and re-raise is legitimate; catching to swallow is not.
The exceptions you will meet
| Exception | Cause |
|---|---|
KeyError | dictionary key does not exist |
IndexError | list position does not exist |
TypeError | wrong kind of value |
ValueError | right type, unusable content — int("abc") |
AttributeError | no such method or attribute |
FileNotFoundError | file is not there |
ZeroDivisionError | division by zero |
TimeoutError | something took too long |
json.JSONDecodeError | text was not valid JSON |
Libraries add their own. Model SDKs typically raise RateLimitError, AuthenticationError and APIError; Pydantic raises ValidationError. Catch those by name once you know which library you are using.
else and finally
try:
f = open("config.json", encoding="utf-8")
except FileNotFoundError:
print("no config")
else:
print("opened fine")
f.close()
finally:
print("done")elseruns only if nothing was raisedfinallyruns whatever happens, including when an exception is on its way out
For files and connections, with is better than finally — it closes for you:
with open("config.json", encoding="utf-8") as f:
data = f.read()Raising your own
def set_severity(value):
if value not in {"P1", "P2", "P3"}:
raise ValueError(f"unknown severity: {value}")
return valueFail early and loudly on bad input. A clear exception at the point of the mistake is far kinder than a confusing one three functions later.
Use the built-in types where they fit — ValueError for bad content, TypeError for the wrong kind of thing. Invent your own only when callers need to catch it specifically:
class ToolExecutionError(Exception):
pass
raise ToolExecutionError("weather tool returned no data")Retrying, which agent code always needs
External calls fail transiently. The standard answer is to retry with a growing wait, so you do not hammer a service that is already struggling.
import time
def call_with_retry(func, attempts=3):
for i in range(attempts):
try:
return func()
except (TimeoutError, ConnectionError) as e:
if i == attempts - 1:
raise
wait = 2 ** i
print(f"attempt {i + 1} failed ({e}), retrying in {wait}s")
time.sleep(wait)Three things make this correct rather than merely present:
- it retries specific transient errors, not everything
- it waits longer each time — 1s, 2s, 4s. This is called exponential backoff
- it re-raises on the last attempt, so a genuine outage is not silently swallowed
Do not retry a ValueError or an authentication failure. Those will fail identically every time; retrying only wastes money.
For production, tenacity gives you this as a decorator with jitter and caps. Understand the loop above first.
Failing softly in an agent
Sometimes the right answer is not to crash but to tell the model what went wrong.
def run_tool(name, args):
try:
return TOOLS[name](**args)
except KeyError:
return f"Error: no tool called {name}"
except TypeError as e:
return f"Error: wrong arguments for {name}: {e}"
except Exception as e:
logger.exception("tool %s failed", name)
return f"Error: {name} failed: {e}"Returning the error as text lets the agent read it, correct itself and try again — which is often exactly what it will do. Note that it is still logged with a full traceback for you, while the model gets a short, useful sentence.
This is the one place where a broad except Exception is defensible, because a tool crash should not kill the whole run. It is defensible only because it logs.
Read the traceback bottom-up
Traceback (most recent call last):
File "agent.py", line 12, in <module>
print(students[10])
IndexError: list index out of rangeThe last line is the answer. The line above is where it happened. Everything else is the trail of how you got there.
Coming from Java or C#
| Concept | Java / C# | Python |
|---|---|---|
| Catch | catch (E e) | except E as e: |
| Cleanup | finally | finally |
| Auto-close | try-with-resources, using | with |
| Throw | throw new E() | raise E() |
| Base type | Exception | Exception |
| Checked exceptions | Java has them | none |
| Re-throw | throw;, throw e; | bare raise |
| Ran-without-error branch | none | else |
The big difference is no checked exceptions. Nothing in a signature tells you what a function can raise, and nothing forces you to handle it. You find out from the documentation, or from production.
The practical consequence: be deliberate. Wrap the calls that genuinely reach outside your process, and let real bugs in your own code crash loudly so you find them.
Common mistakes
except Exception: pass, hiding real bugs.- Catching too broadly, too early, so a typo looks like an API failure.
- Retrying non-transient errors such as a bad key.
- Not re-raising on the final attempt, turning an outage into a silent empty result.
logger.errorwherelogger.exceptionwould have given you the traceback.finallyfor closing files wherewithis cleaner.
Practise this
- Trigger
KeyError,ValueErrorandZeroDivisionErroron purpose and catch each by name. - Write a function that raises
ValueErrorfor bad input, and catch it at the call site. - Write the retry loop from memory, with backoff and a re-raise at the end.
- Take an
except Exception: pass— write one if you have to — and replace it with something specific that logs.