skip to content

Inside an except block, what does a bare `raise` statement re-raise?

level: seniorimportance: should knowfreq 44%

answer

  1. No expression means no new exception
  2. It reads interpreter state, not a variable
  3. Works even with no name in scope
  4. Fails when nothing is being handled
  5. Log-and-rethrow, never log-and-swallow

basics

~10 s

A bare raise re-raises the exception currently being handled, taken from the interpreter's own exception state rather than from any variable. Outside an active handler it raises RuntimeError: No active exception to reraise.

solid answer

~40 s

A bare `raise` re-raises the exception the interpreter is currently handling. It reads that from the thread's exception state — the same state `sys.exc_info()` reports — not from the `as` name, which is why it still works in a helper function called from the handler and why the handler's implicit deletion of its `as` target never gets in the way. If nothing is being handled, it raises `RuntimeError: No active exception to reraise`, and that includes the `finally` clause of a statement whose handler has already completed, because the exception state is cleared when the handler exits. Compared with `raise err`, the bare form does not add the handler's own `raise` line as an extra traceback entry, so the reported stack points at the original failure rather than at the code that logged it.

code

python · 19 lines
python
def rethrow():
    raise

def observe():
    try:
        raise TimeoutError("upstream did not answer")
    except TimeoutError:
        print("counted a timeout")
        rethrow()

try:
    observe()
except TimeoutError as err:
    print("caller sees:", err)

try:
    raise
except RuntimeError as problem:
    print(problem)

go deeper

for a junior

Learn the shape: a raise on its own inside a handler sends the same exception onward. Use it whenever your handler only logs or counts the failure and cannot actually fix it.

for a middle

Explain where the exception comes from — the interpreter's current exception state, not the as name — and why the bare form keeps the traceback pointing at the original failure instead of at your handler.

for a senior

Demonstrate the operational judgment: which layer is entitled to decide a failure is handled, why log-and-swallow produces silent degradation that survives release after release, and why re-logging at every layer is its own kind of noise.

for a principal

Own the error-propagation contract across services and teams: where failures are observed, where they are translated into domain errors, and how you keep handlers from quietly absorbing failures that callers' retry and fallback policies were supposed to see.

`raise` with no expression is the language's re-raise statement, and it means something quite specific: *raise the exception that is currently being handled, unchanged*. ## Where it gets the exception from Not from the `as` name. The interpreter keeps a per-thread "currently handled exception" while a handler body is running — the same state `sys.exc_info()` reports — and a bare `raise` reads it from there. Two useful consequences follow. First, it works even where no name is in scope. A helper called from inside a handler can re-raise: ```python def rethrow(): raise # re-raises whatever the caller is handling try: risky() except TimeoutError: rethrow() ``` Second, it is immune to the handler's implicit deletion of its `as` target. The name is unbound when the block ends; the exception state is what the bare form uses, so re-raising never depends on a binding that is about to disappear. ## When it fails If there is no exception being handled, a bare `raise` raises `RuntimeError: No active exception to reraise`. The obvious case is a bare `raise` at module level. The non-obvious one is a `finally` clause that runs after the handler already completed: ```python try: raise ValueError("boom") except ValueError: pass finally: raise # RuntimeError: no exception is being handled any more ``` The exception state is cleared when the handler body finishes, so by `finally` there is nothing to re-raise. The state is also saved and restored around nested handlers: inside a nested handler `sys.exc_info()` reports the inner exception, and once it exits, the outer one again. ## Bare `raise` versus `raise err` Both send the same object onward, and both preserve `__traceback__` — the notion that `raise err` "loses the traceback" is a Python 2 memory and is wrong on Python 3. The real difference is noise. `raise err` is a fresh raise from the handler's line, so that line is appended to the traceback: the printed traceback gains one extra frame entry at the top, naming the `raise err` line in the handler, above the entry for the call that actually failed. Count them and the difference is exact: re-raising a three-frame failure with a bare `raise` still reports three frames, while `raise err` reports four. The bare form omits that extra entry, so the top of the reported stack is the code that actually failed. When you are staring at the fifth intermittent timeout of the week in an ad-auction bidder, the difference between a traceback pointing at the failing call and one pointing at the line that logged it is the difference between a five-minute diagnosis and an afternoon. `raise err` also has a subtler cost: if `err` was rebound, wrapped or replaced anywhere in the handler, you are re-raising whatever it points at now rather than what was caught. The bare form cannot drift. ## The log-and-rethrow pattern This is where the statement earns its keep: ```python try: submit(auction) except TimeoutError: timeouts += 1 logging.warning("bid submission timed out", exc_info=True) raise ``` The handler exists to *observe* the failure — count it, log it, tag it — and then let the caller's own policy decide what happens. Retry, fallback bid, or give up is not this function's decision. The anti-pattern it replaces is the handler that logs and then swallows. That is the failure mode behind most "the system was silently degraded for a while" incidents: the error is recorded somewhere nobody reads, the caller sees a plausible-looking empty result, and the bad behaviour survives an entire three-week release train before anyone connects the dots. If a handler cannot actually recover, it must re-raise. The mirror-image mistake is logging *and* re-raising at every level, which produces the same stack trace five times in the log. Pick one layer to observe at — usually the boundary that knows what the operation meant — and let the exception travel untouched through the rest. ## Adding context without losing the original If you need to say more than the original exception says, you have two honest options: attach a note to the exception before the bare `raise`, or raise a new, more meaningful exception that carries the original as its cause. Both keep the original failure visible. What you must not do is catch, log, and return a normal-looking value — that is the one choice that destroys information permanently. ## What an interviewer is checking The syntax is trivial; the judgment is not. A senior answer covers where the exception comes from, the `RuntimeError` when nothing is active, and — most importantly — the reasoning about *which layer* is entitled to decide that a failure has been handled.

  • Does `raise err` lose the original traceback the way people often claim?
    No — that is a Python 2 memory. In Python 3 the exception carries its own traceback on `__traceback__`, and re-raising the same object keeps it. The real difference is that `raise err` appends the handler's own line as an extra traceback entry, so the top of the reported stack is the code that logged the failure rather than the code that caused it. The bare form avoids that.
  • When is it acceptable for a handler to swallow an exception rather than re-raise it?
    Only when the handler genuinely recovers — it has a fallback value, an alternative path, or the failure is expected and irrelevant to the caller. The test is whether the caller can still trust the return value. If it cannot, swallowing turns a loud failure into a silent wrong answer, which is far more expensive to diagnose than a propagating exception.
  • Why does a bare raise inside a finally clause sometimes raise RuntimeError?
    Because the interpreter's currently-handled-exception state is cleared once the handler body finishes. If the statement caught and handled the exception, nothing is active by the time `finally` runs, so a bare `raise` there has nothing to re-raise and reports `No active exception to reraise`. It works in `finally` only when an exception is genuinely still propagating.

A bare raise is signing the incident report and passing it up the chain unchanged; raise err re-types the report on your own letterhead, so your desk appears at the top of it.

saying these in an interview costs you the question

  • Says `raise err` discards the traceback in Python 3
  • Thinks a bare raise creates a fresh generic exception
  • Logs the error and returns a normal-looking value instead
  • Believes a bare raise needs the as name in scope
  • Re-logs the same exception at every layer it passes

context