Programming with AI
ENRU
2.8

Exceptions and error handling

where an error is raised and where it is caught

What it is

An exception is a way of saying that work cannot continue. A raise line raises an error, a try/except block catches it. Between the two, execution jumps over all the code in between.

This mechanism is read through two questions: where the error is raised and where it is caught. There may be a dozen functions between them — which is exactly what a traceback shows (article 2.12).

What it looks like in code

def parse_port(raw: str) -> int:
    try:
        port = int(raw)
    except ValueError:
        raise ValueError(f"port must be a number, got: {raw!r}") from None
    if not 1 <= port <= 65535:
        raise ValueError(f"port out of range: {port}")
    return port


for raw in ["8080", "abc", "99999"]:
    try:
        print(raw, "->", parse_port(raw))
    except ValueError as e:
        print(raw, "-> error:", e)

Output:

measured
8080 -> 8080
abc -> error: port must be a number, got: 'abc'
99999 -> error: port out of range: 99999

How to read it

try: — "attempt this". If something inside fails, execution moves to except immediately and the rest of the try is skipped.

except ValueError: catches only errors of that kind. Others fly past and go further up.

raise raises an error. Here the function turns the opaque invalid literal for int() into a clear message that includes the value that didn't fit.

as e puts the error object into a variable so the text can be read.

finally: runs either way, error or not. It is for closing files and connections.

The hierarchy of errors

Errors form a tree: ValueError and KeyError are special cases of Exception. So except Exception catches almost everything, while except ValueError catches only its own.

KindWhen it happens
ValueErrorthe value is the wrong shape: int("abc")
KeyErrorno such key in the dictionary
TypeErrorthe wrong type: a string added to a number
AttributeErrorno such field or method (often on None)
FileNotFoundErrorthe file is not there
TimeoutErrorthe answer never came

These six cover most of what you will meet while reading.

Catch it where you know what to do

The rule for checking where try belongs: catch an error where there is a sensible answer to "and now what?".

In the example, parse_port does not catch its own range error — it has no idea what to do with a bad port. The calling code does: print a message and move on to the next value.

Which is why a try wrapped around every line is a sign the author never thought about who would react to the error, or how.

What to tell the AI

Prompt
Which exceptions can this function raise, and where are they
caught? If nowhere, say so.
Prompt
There is an except Exception here. Which specific errors
did you expect to catch? Narrow it down to those.

The second prompt is a working way to fix over-broad except clauses: the AI can usually name the specific kinds, if asked.

What to look for

except Exception: pass — the error is swallowed whole. The code doesn't crash and therefore looks like it works.

except with no type. A bare except: catches even a Ctrl+C interrupt.

An error caught but not recorded. If the except has neither a log nor a re-raise, the information is lost forever.

A message without the data. raise ValueError("invalid value") — which one? A good message always contains the thing that didn't fit.

A try wrapped around twenty lines. It is unclear which of them can fail. Narrowing the try to one or two lines is the cheapest way to make the code readable.

Cheat sheet

  • raise raises, try/except catches, execution jumps between them.

  • except ValueError catches only its own, except Exception almost everything.

  • Six common errors: Value · Key · Type · Attribute · FileNotFound · Timeout.

  • Catch where you know what to do next.

  • except Exception: pass — the error is swallowed and the code lies.

  • An error message should always name the value that didn't fit.