From 9e72c669929066467548d521beca898a4f2fc667 Mon Sep 17 00:00:00 2001 From: ATOM00blue <219721791+ATOM00blue@users.noreply.github.com> Date: Fri, 22 May 2026 07:51:43 +0530 Subject: [PATCH 1/2] fix: reraise underlying exception when TryAgain wraps a cause When reraise=True and TryAgain is raised from within an except block, RetryError.reraise() now surfaces the underlying exception that caused the retry once attempts are exhausted, rather than the opaque TryAgain sentinel. A bare TryAgain raised without an active exception keeps reraising TryAgain as before. Fixes #544 --- ...again-underlying-exc-e21a26ee95bad0b3.yaml | 8 +++++ tenacity/__init__.py | 8 +++++ tests/test_asyncio.py | 22 ++++++++++++ tests/test_tenacity.py | 36 +++++++++++++++++++ 4 files changed, 74 insertions(+) create mode 100644 releasenotes/notes/fix-reraise-tryagain-underlying-exc-e21a26ee95bad0b3.yaml diff --git a/releasenotes/notes/fix-reraise-tryagain-underlying-exc-e21a26ee95bad0b3.yaml b/releasenotes/notes/fix-reraise-tryagain-underlying-exc-e21a26ee95bad0b3.yaml new file mode 100644 index 00000000..ac8f239a --- /dev/null +++ b/releasenotes/notes/fix-reraise-tryagain-underlying-exc-e21a26ee95bad0b3.yaml @@ -0,0 +1,8 @@ +--- +fixes: + - | + When ``reraise=True`` and ``TryAgain`` is raised from within an ``except`` + block, the underlying exception that triggered the retry is now reraised + once attempts are exhausted, instead of the opaque ``TryAgain`` sentinel. + A bare ``TryAgain`` raised without an active exception keeps reraising + ``TryAgain`` as before. diff --git a/tenacity/__init__.py b/tenacity/__init__.py index 282e6dae..4b55679f 100644 --- a/tenacity/__init__.py +++ b/tenacity/__init__.py @@ -185,6 +185,14 @@ def __init__(self, last_attempt: "Future") -> None: def reraise(self) -> t.NoReturn: if self.last_attempt.failed: + exc = self.last_attempt.exception() + # When the user explicitly raises TryAgain (typically from within + # an "except" block), surface the underlying exception that caused + # the retry rather than the opaque TryAgain sentinel. + if isinstance(exc, TryAgain): + cause = exc.__cause__ or exc.__context__ + if cause is not None: + raise cause raise self.last_attempt.result() raise self diff --git a/tests/test_asyncio.py b/tests/test_asyncio.py index 7b5c6416..890610ac 100644 --- a/tests/test_asyncio.py +++ b/tests/test_asyncio.py @@ -238,6 +238,28 @@ class CustomError(Exception): else: raise Exception + @asynctest + async def test_reraise_try_again_with_cause(self) -> None: + # When TryAgain is raised from within an "except" block, reraise=True + # should surface the underlying exception rather than TryAgain itself. + class UnderlyingError(Exception): + pass + + async def _test() -> None: + try: + raise UnderlyingError("boom") + except UnderlyingError: + # Implicit chaining via __context__ is exactly what we test. + raise tenacity.TryAgain # noqa: B904 + + retrying = tasyncio.AsyncRetrying( + stop=stop_after_attempt(2), + retry=tenacity.retry_never, + reraise=True, + ) + with pytest.raises(UnderlyingError): + await retrying(_test) + @asynctest async def test_sleeps(self) -> None: start = current_time_ms() diff --git a/tests/test_tenacity.py b/tests/test_tenacity.py index 6a397392..7ff47626 100644 --- a/tests/test_tenacity.py +++ b/tests/test_tenacity.py @@ -857,6 +857,42 @@ def _r() -> None: self.assertRaises(tenacity.RetryError, r, _r) self.assertEqual(5, r.statistics["attempt_number"]) + def test_retry_try_again_with_cause_reraise(self) -> None: + # When TryAgain is raised from within an "except" block, reraise=True + # should surface the underlying exception rather than TryAgain itself. + class UnderlyingError(Exception): + pass + + def _r() -> None: + try: + raise UnderlyingError("boom") + except UnderlyingError: + # Implicit chaining via __context__ is exactly what we test. + raise tenacity.TryAgain # noqa: B904 + + r = Retrying( + stop=tenacity.stop_after_attempt(5), + retry=tenacity.retry_never, + reraise=True, + ) + self.assertRaises(UnderlyingError, r, _r) + self.assertEqual(5, r.statistics["attempt_number"]) + + def test_retry_try_again_from_cause_reraise(self) -> None: + # An explicit "raise TryAgain from exc" should also be unwrapped. + class UnderlyingError(Exception): + pass + + def _r() -> None: + raise tenacity.TryAgain from UnderlyingError("boom") + + r = Retrying( + stop=tenacity.stop_after_attempt(5), + retry=tenacity.retry_never, + reraise=True, + ) + self.assertRaises(UnderlyingError, r, _r) + def test_retry_try_again_forever_reraise(self) -> None: def _r() -> None: raise tenacity.TryAgain From e3de85cf08b1c9cf5195aeb1ec449a596994d617 Mon Sep 17 00:00:00 2001 From: Julien Danjou Date: Fri, 22 May 2026 09:04:54 +0200 Subject: [PATCH 2/2] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- tenacity/__init__.py | 2 +- tests/test_asyncio.py | 1 + tests/test_tenacity.py | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/tenacity/__init__.py b/tenacity/__init__.py index 4b55679f..c52d6ba0 100644 --- a/tenacity/__init__.py +++ b/tenacity/__init__.py @@ -192,7 +192,7 @@ def reraise(self) -> t.NoReturn: if isinstance(exc, TryAgain): cause = exc.__cause__ or exc.__context__ if cause is not None: - raise cause + raise cause.with_traceback(cause.__traceback__) from None raise self.last_attempt.result() raise self diff --git a/tests/test_asyncio.py b/tests/test_asyncio.py index 890610ac..c6d27865 100644 --- a/tests/test_asyncio.py +++ b/tests/test_asyncio.py @@ -259,6 +259,7 @@ async def _test() -> None: ) with pytest.raises(UnderlyingError): await retrying(_test) + self.assertEqual(2, retrying.statistics["attempt_number"]) @asynctest async def test_sleeps(self) -> None: diff --git a/tests/test_tenacity.py b/tests/test_tenacity.py index 7ff47626..31e7f432 100644 --- a/tests/test_tenacity.py +++ b/tests/test_tenacity.py @@ -892,6 +892,7 @@ def _r() -> None: reraise=True, ) self.assertRaises(UnderlyingError, r, _r) + self.assertEqual(5, r.statistics["attempt_number"]) def test_retry_try_again_forever_reraise(self) -> None: def _r() -> None: