Skip to content

fix: keep the exception chain when reraising a TryAgain cause - #764

Open
Enesuygurs wants to merge 1 commit into
jd:mainfrom
Enesuygurs:fix/tryagain-reraise-keep-exception-chain
Open

Enesuygurs wants to merge 1 commit into
jd:mainfrom
Enesuygurs:fix/tryagain-reraise-keep-exception-chain

Conversation

@Enesuygurs

Copy link
Copy Markdown

Problem

Since #640, reraise=True surfaces the exception that triggered a TryAgain instead of the TryAgain itself. It does this with raise cause... from None, which mutates that exception: its __cause__ is reset to None and __suppress_context__ is forced to True. The traceback then loses whatever led to the error.

from tenacity import Retrying, TryAgain, stop_after_attempt

def load_config():
    try:
        raise OSError("disk unavailable")
    except OSError as e:
        raise KeyError("config") from e

def call():
    try:
        load_config()
    except KeyError:
        raise TryAgain

Retrying(stop=stop_after_attempt(2), reraise=True)(call)
# KeyError('config'), but __cause__ is None: the OSError and the
# "direct cause" section are gone from the traceback.

The same applies to an implicit context (raise KeyError inside except OSError): the "During handling of the above exception" section is suppressed. Retrying the same function without TryAgain keeps the chain intact, because that path (raise self.last_attempt.result()) does not use from None.

Fix

Drop from None in RetryError.reraise() and raise the exception as is. Its own __cause__ and __context__ are left untouched, which matches the non-TryAgain path. Behaviour is otherwise unchanged: the same exception type and instance are raised, and a bare TryAgain still reraises TryAgain. A reno release note is included.

Note: #730 edits the line two above this one in the same hunk. Whichever of the two lands second will need a trivial rebase.

Tests

I added regression tests in tests/test_tenacity.py for the explicit chain (raise ... from e) and the implicit context, and one in tests/test_asyncio.py for the explicit chain with AsyncRetrying. They fail without the change and pass with it.

  • pytest: 188 passed, 15 subtests passed
  • ruff check . / ruff format --check .: pass
  • mypy: no issues found in 19 source files
  • sphinx-build -W doctest and html: pass

RetryError.reraise() surfaced the exception behind a TryAgain with
"raise ... from None", which overwrote that exception's own __cause__
and suppressed its __context__, so the traceback no longer showed what
led to it. Raise it as is, like the non-TryAgain path already does.
@mergify

mergify Bot commented Oct 21, 2026

Copy link
Copy Markdown
Contributor

Tick the box to add this pull request to the merge queue (same as @mergifyio queue).

  • Queue this pull request

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant