> ## Content Index
> Fetch the complete content index at: https://bitstales.eu/llms.txt
> Use this file to discover other available public pages before exploring further.

# It's not you, it's my catch: an advice column for exceptions
- URL: https://bitstales.eu/en/try-catch-when-not-to/
- Published: 2025-09-05T19:12:00.000Z
- Updated: 2025-09-05T19:12:00.000Z
- Description: Empty catches, bare excepts, endless retries, lost causes and return inside finally: Dr Finally answers your code's letters. Because catch is not always the answer.
- Author: Vittorio
- Tags: Programming, Error Handling, #en

Welcome to **Dear Runtime**, the advice column where your code shares its relationship troubles and a professional tries to put the pieces back together. Answering today is **Dr Finally**: thirty years of experience in exception handling and one firm belief, which is that `catch` is not always the right answer.

Because a `try/catch` is a relationship in every sense. Someone throws a problem, someone receives it, and there is the eternal question: *do we talk about it now, or pretend nothing happened?* On to the letters.

## "He never tells me anything"

> Dear Doctor, my colleague wrote this three years ago. Since then orders have been disappearing and nobody knows why. — *Julia, 31, Java backend*

```java
try {
    orders.save(order);
} catch (Exception e) {
    // should never happen
}
```

Dear Julia, this is the relationship where problems are never addressed. The database is down, the connection has timed out, the disk is full, and he says "everything's fine, why?". The program carries on as if the order had been saved, the user gets a confirmation and the customer will never get their parcel. The problem has not gone away: it has been **postponed**, and when it resurfaces nobody will know where to look.

The jargon for this is *Pokémon exception handling*, because "gotta catch 'em all". The comment "should never happen" is the equivalent of "but we never argue": true only until someone checks.

**The rule:** if you catch an exception you have to *do something*: actually recover, add context and rethrow, or record it and respond sensibly. An empty `catch` block is not error handling. It is ghosting.

## "She's jealous: she intercepts all my calls"

> Doctor, my Python worker won't stop any more. Not with Ctrl+C, not with `sys.exit()`. I have to use `kill -9`. — *Mark, 27, data engineer*

```python
while True:
    try:
        process_next_batch()
    except:          # bare "except": catches EVERYTHING
        pass
```

Dear Mark, your `except` is the jealous partner who answers every phone call, including the ones from the fire brigade. In Python a bare `except:` catches `BaseException`, which includes `KeyboardInterrupt` (Ctrl+C) and `SystemExit` (`sys.exit()`). In practice, the program won't let you leave.

The exception hierarchy is designed precisely to avoid this: `Exception` holds the "normal" errors, while shutdown requests live outside it. Since version 3.8 `asyncio.CancelledError` also derives from `BaseException`, so `except Exception` no longer swallows task cancellation by accident. `except BaseException` still does, though.

The same goes for `catch (Throwable t)` in Java, which also catches `OutOfMemoryError` and `StackOverflowError`. Those are situations the JVM often cannot reliably recover from. And if you catch `InterruptedException`, restore the flag. Otherwise whoever wanted to stop the thread will never find out:

```java
catch (InterruptedException e) {
    Thread.currentThread().interrupt();   // "I heard you, and I'll pass it on"
    return;
}
```

**The rule:** catch the **most specific** type you know how to handle. `FileNotFoundError`, not `Exception`. Jealousy is not an error-handling strategy.

## "He tells everyone about our arguments"

> Doctor, when something goes wrong in our service, the same stack trace appears four times in the logs, at different levels. During night shifts on call it feels like four incidents. — *The SRE team at a company that prefers to stay anonymous*

```java
// repository
catch (SQLException e) { log.error("db error", e); throw e; }
// service
catch (SQLException e) { log.error("save failed", e); throw e; }
// controller
catch (Exception e) { log.error("request failed", e); throw e; }
```

Dear SREs, this is the couple who, after every argument, tells their mother, their best friend, a colleague and the barista. In the end everyone knows four versions of the fight, and nobody knows how it ended. Every layer logs the error *and* rethrows it, so one problem produces four log entries, four alerts and an hour lost working out that it was a single thing.

**The rule:** either handle an error or propagate it. Not both. Log it **once**, where it is actually handled, which is usually the edge of the system: the HTTP handler, the main loop, the queue consumer. If an intermediate layer has something to add, it should add *context*, not another log line.

## "She dumped me with a two-word text"

> Doctor, all I find in the logs is `RuntimeException: DB error`. No details, no cause. What happened? — *Frances, 35, tech lead*

```java
catch (SQLException e) {
    throw new RuntimeException("DB error");   // and the cause?
}
```

Dear Frances, you were dumped by text: "Doesn't work. Bye". The original exception, with the SQL code, the violated constraint and the stack trace, was thrown away. Translating an exception into a higher-level one is perfectly fine (it's the Adapter pattern applied to errors), but the cause must be **attached**. Every language has its way of doing it:

```java
throw new OrderNotSaved("saving order " + id, e);   // Java: the cause stays linked
```

```python
raise OrderNotSaved(f"saving order {id}") from e      # Python: "The above exception was the direct cause..."
```

```go
return fmt.Errorf("saving order %d: %w", id, err)     // Go: %w, then errors.Is / errors.As
```

Python also has `raise ... from None`, which hides the cause on purpose. That is deliberate ghosting: sometimes it makes sense, for example when the cause contains sensitive data, but you need to know why you are doing it.

## "We keep breaking up and getting back together"

> Doctor, our client retries forever when the payment service does not respond. Yesterday the service recovered and we knocked it straight back down. And one customer was charged three times. — *Luke, 40, startup CTO*

```python
while True:
    try:
        return charge(card, amount)
    except Exception:
        continue        # let's try again, maybe it'll work this time
```

Dear Luke, this is the on-again, off-again ex, distributed edition. Three problems:

1. **No pause.** A thousand clients retrying all at the same instant create a flood of requests (a *thundering herd*) that knocks the service over the moment it gets back up.
2. **No distinction.** A timeout may be temporary. "Card declined" is not. Retrying a permanent error is like calling someone who has blocked you: nothing changes, it just makes more noise.
3. **No idempotency.** If the first request got through and only the response was lost, the second attempt charges again. Hence the three payments.

```python
import random, time

def with_retry(op, attempts=5, base=0.2):
    for n in range(attempts):
        try:
            return op()
        except (TimeoutError, ConnectionError):          # transient errors only
            if n == attempts - 1:
                raise
            time.sleep(base * 2 ** n * random.uniform(0.5, 1.5))   # exponential backoff + jitter

with_retry(lambda: charge(card, amount, idempotency_key=order.id))
```

Add a *circuit breaker*: after too many failures it stops trying for a while, just like the breaker in your fuse box. Sometimes the healthiest thing is to take a break.

## "He uses a crisis to decide where to have dinner"

> Doctor, my parser checks whether a string is a number by trying to convert it and catching the exception. It does this millions of times a second. The profiler says we spend 40% of our time building stack traces. — *Andrew, 29, developer*

Dear Andrew, exceptions are for *exceptional* situations, not ordinary traffic. In Java, creating an exception is expensive: the constructor calls `fillInStackTrace()`, which walks the whole stack. In C++ the "zero-cost" model costs nothing when everything goes well, but throwing is very expensive, because stack unwinding goes through tables and lookups. It's like starting a relationship crisis every evening to choose between pizza and sushi: you get an answer, but at a high price.

There are cultural differences, though. In Python the EAFP style (*Easier to Ask Forgiveness than Permission*) is idiomatic: `try: d[k] except KeyError:` is normal, and even the `for` loop ends with an exception, `StopIteration`. Every language has its customs. What matters is not importing another's without understanding them.

## "I found out she only stayed out of habit"

> Doctor, we have a `catch (NullPointerException e)` that "fixes" a crash in production. Since then some users' data has been wrong, but nobody can say which. — *Sarah, 33, QA*

Dear Sarah, a `NullPointerException`, an `IndexError` or a failed assertion are not life events to be handled: they are **bugs**. Catching them keeps a program running when its internal state is already wrong, and from then on every operation risks writing corrupt data. It's the relationship that carries on out of inertia: nobody trusts anyone any more, but nobody wants to be the first to say so.

Whole ecosystems are built on the opposite principle. Erlang and Elixir follow the *let it crash* philosophy: a process that finds itself in an unexpected state dies at once, and a *supervisor* starts a fresh, clean one. Rust draws a hard line between `Result` (an expected error, to be handled) and `panic!` (a bug, which propagates). On a server, systemd with `Restart=on-failure` and Kubernetes with its restarts do the same thing at the process level.

**The rule:** bugs are not caught, they are fixed. Sometimes the healthiest thing is to break up: a clean crash and a restart beat living with corrupt data.

## "He left without giving me back the keys"

> Doctor, after a few hours our service runs out of file descriptors. Every time a request fails, a file is left open. — *Paul, 38, sysadmin*

Dear Paul, even when a relationship ends badly, the keys must be returned. Releasing resources must not depend on how things went: that is what `finally` is for and, better still, the constructs that make it automatic. `with` in Python, try-with-resources in Java, `defer` in Go, RAII in C++ and Rust:

```python
with open(path) as f:        # the file is closed even if something goes wrong
    data = parse(f)
```

Watch out for a little-known trap, though: a `return` inside `finally` makes the exception in flight disappear.

```python
def save():
    try:
        raise IOError("disk full")
    finally:
        return "all good"        # the IOError vanishes: nobody will ever know
```

It's the "all good" said with a smile while the house burns down. It happens in Java too. Python has decided to step in: with PEP 765, Python 3.14, due out next month, will emit a `SyntaxWarning` for `return`, `break` and `continue` that exit a `finally` block.

## So when do you catch? (The couples that work)

After so many sad letters, some good news: the right `catch` exists, and it is the one with a **plan**.

- **At the edges of the system.** The HTTP handler that turns an exception into a 500 response with a correlation ID, the CLI that prints a clear message and exits with a non-zero code, the UI that shows "try again later". This is where you log the error, once.
- **When there is a real plan B.** The user's config file doesn't exist (`FileNotFoundError`, not `Exception`)? Use the defaults. The remote cache isn't responding? Read from the database.
- **To translate between layers**, keeping the cause: `SQLException` becomes `OrderNotSaved`, so the domain layer doesn't need to know about SQL.
- **To add context**: "saving order 4812" is far more useful than "error".

Some languages chose not to use exceptions at all for expected errors. Go uses `if err != nil`: explicit communication at every step, a bit tedious but honest. Rust uses `Result` and the `?` operator: say straight away that something is wrong, without drama, and hand the problem to whoever can deal with it. Java has *checked exceptions*, a kind of prenuptial agreement: the terms are written in the method signature, and people have argued for thirty years about whether that is a good idea. C# and Kotlin, tellingly, decided to do without them.

## Ten rules for a healthy relationship

1. Catch only what you know how to handle.
2. Catch the most specific type, never a bare `except:` or `Throwable`.
3. Either handle or propagate: not both.
4. Log the error once, at the edge of the system.
5. Never lose the cause: `from e`, `%w`, `new X(msg, e)`.
6. Retry only transient errors, with backoff, jitter, a limit and idempotency.
7. Bugs are not caught: they are fixed.
8. Release resources with `with`, `defer` or RAII, and never a `return` inside `finally`.
9. Exceptions are for exceptional cases, not control flow (local customs excepted).
10. Sometimes the healthiest thing is to break up: a clean crash and a good supervisor.

With love and a full stack trace,  
**Dr Finally** 💌