JJoeven

Curriculum/Python

Errors and try/except

Catch specific errors, clean up with finally, and turn tool failures into dicts the agent loop can read.

beginner21 min22 / 37

An exception is an error that stops normal flow. If nobody catches it, the whole program dies. The last line of the message is usually the type and a short reason: ValueError: only https allowed. Read that line first. Then read the traceback from the bottom up to see which of your lines threw.

Tools fail all the time: bad URL, missing key, timeout. In an agent, that should rarely kill the process. You catch the error at the tool edge, save it as data, and let the loop choose: retry, skip, or stop.

Catch, then keep going
tryresultexcepterror dict

If the tool fails, catch it and return a dict. The loop reads ok. It should not crash.

Catch, then keep going

A crash inside search that you never wrap becomes a crashed loop. The model never sees “search timed out.” You see a traceback in a terminal. Those are different products. One is an agent. The other is a broken script.

What you will learn

  • try, except, else, finally
  • raise with a custom message
  • Tool failure as a dict vs an exception
  • Never write a bare except:

try / except / else / finally

python
try:
    result = read_url(url)
except ValueError as e:
    result = class="tok-s">"failed: " + str(e)
else:
    print(class="tok-s">"no error")
finally:
    print(class="tok-s">"tool finished")
WordWhen it runs
tryThe risky work
except ValueErrorOnly if that error type happened
elseOnly if try had no error
finallyAlways — success, failure, or return

Catch a specific type. except ValueError is clear. except Exception is wide. Start specific. KeyError, TypeError, json.JSONDecodeError, ValueError are the usual agent set.

as e names the error object. str(e) is the message. type(e).__name__ is the type as text, useful in a dict you print.

else is optional. Use it when the success path should not run if you caught. Putting success code in try also works, but then a bug in the success path is caught by the same except. else avoids hiding those bugs.

finally is for cleanup: close, print “finished,” reset a flag. A return inside try still runs finally before the function actually returns. That is why the live box prints finished on every URL.

raise and a custom message

raise throws an error. You can attach a message (a string that explains what went wrong).

python
def read_url(url):
    if not url.startswith(class="tok-s">"https://"):
        raise ValueError(class="tok-s">"only https allowed")
    return class="tok-s">"ok body"

raise ValueError("only https allowed") is a built-in error type plus your message. The agent can print that message or store it. Write messages that help a person: what was wrong, what is allowed. "bad" is not a message.

Inside except, a bare raise throws the same error again. Use that after you log, if you still cannot handle it. That is for unexpected bugs in your loop. Tool failures should become dicts instead, so the loop keeps running.

You can also raise ValueError("...") from e to chain. Skip that until you need it. A single clear raise is enough.

Do not put secrets in the message. Raise ValueError("missing API_KEY"), not ValueError("bad key " + key). Traces copy messages.

Dict vs exception

Two ways a tool can fail:

StyleExampleEffect
Exceptionraise ValueError("timed out")Must be caught, or the program stops
Dict{"ok": False, "error": "timed out"}The loop keeps running and reads ok

At the tool edge, convert exceptions to data: a dict with ok and error. Inside the while loop, work with data. That is easier to test. Tests pass a dict. They do not have to catch.

Inside a tool, raise is still fine to stop bad input. call_tool catches and wraps. One wrapper, many tools.

You can list more than one type: except (ValueError, TypeError) as e:. Keep the tuple tight. Do not add Exception to that tuple “just in case.” Order matters if you stack except blocks: specific types first, wider later. except Exception then except ValueError will never hit ValueError, because Exception already caught it. Put ValueError first.

else on try is “no error.” It is not the same as else on if. If you skip else and put success code after the whole try/except/finally, finally has already run. The live box uses return inside except and else, and print in finally. Copy that shape for tools.

Walkthrough: one wrapper, many failures

python
import json

def call_tool(name, raw):
    try:
        args = json.loads(raw)
        if name == class="tok-s">"read_url":
            return {class="tok-s">"ok": True, class="tok-s">"body": class="tok-s">"ok body"}
        raise ValueError(class="tok-s">"unknown tool")
    except json.JSONDecodeError as e:
        return {class="tok-s">"ok": False, class="tok-s">"error": class="tok-s">"bad json"}
    except ValueError as e:
        return {class="tok-s">"ok": False, class="tok-s">"error": str(e)}

JSON parse errors belong in the same wrapper. JSONDecodeError is expected from models. Catch it. Do not catch NameError in the same bucket. NameError means you typo’d a variable. That should fail the test, not become an observation.

TypeUsually meansCatch at tool edge?
ValueErrorbad input, policy rejectyes
KeyErrormissing required fieldyes, or prevent with .get
TypeErrorwrong type or extra ** keyyes at executor
json.JSONDecodeErrormodel text was not JSONyes
NameErroryour typono — let tests fail
KeyboardInterruptuser stopped the programnever catch to swallow

Never a bare except

except: with no type catches everything, including errors you should not hide: KeyboardInterrupt, SystemExit, memory errors, syntax bugs you introduced.

Never write a bare except:. Catch ValueError, KeyError, or another real type. Then log it. Swallowing every error is how agents “do nothing” and leave no trace. A silent except: pass is a defect, not a safety feature.

What goes wrong

  • Bare except:.
  • Catching too wide and hiding bugs.
  • Forgetting str(e), then storing the error object in JSON (dumps may fail).
  • Raising in the loop instead of returning a dict.
  • Catching Exception around the whole agent, so nothing ever crashes even when your parser is wrong.
  • Wide except above a specific one, so the specific block is dead.
  • Putting success logic in try so a later KeyError looks like a tool failure.

Always store str(e) in the dict. An exception object is not JSON. json.dumps({"error": e}) can fail and hide the original error.

Live PythonOpen full playgroundpython
Output
Run to execute this in your browser. Nothing is sent to a server.

Each call prints finished because finally always runs. Bad URLs return a dict, not a crash. Change the successful URL and watch else still return {"ok": True, ...}.

How agents use this

Tool failures are normal. The model should see “search timed out” as a row in the transcript, not as a crashed Python process. Catch at call_tool. Return {"ok": False, "error": ...}. Retry only when it makes sense. Let true bugs in your loop still show up. A custom message on raise is how you say what went wrong in one short line.

JSON parse errors belong in the same wrapper. JSONDecodeError is expected from models. Catch it. Do not catch NameError in the same bucket.

Print finished or append a trace row in finally if you need “this tool ended” even when it raised. Then the log has a close parenthesis. Agents that crash mid-tool with no row look like they hung. They did not hang. They died without logging.

Retry policy sits above the wrapper, in the loop, using the dict. if not obs["ok"] and "timeout" in obs["error"] and used < 3: then try again. The wrapper does not retry. If it did, a single user turn could hide a dozen failed calls. One catch, one dict, one loop decision.

Do not wrap the whole while in try/except Exception. A bug in your prompt builder would then become an observation forever, and you would ship it. Wrap tools. Let the loop crash in tests when you are wrong. That split is the whole lesson applied at agent scale.

Check your understanding

What is wrong with writing except: with no error type?