Skip to content

Exception chain lost when retry mechanism re-raises non-retryable errors #880

@fernandocorreia-galileo

Description

Environment details

Description

The retry mechanism clears the __cause__ attribute when re-raising non-retryable exceptions, breaking explicit exception chaining created with raise ... from ....

Steps to reproduce

  1. Use the @retry_async.AsyncRetry decorator
  2. Raise a non-retryable exception with a cause
  3. Catch the exception and try to read or log its cause

Code example

import asyncio
from google.api_core import retry_async, exceptions

@retry_async.AsyncRetry(predicate=retry_async.if_exception_type(exceptions.InternalServerError))
async def example():
    try:
        raise exceptions.Unauthenticated("Invalid credentials")
    except exceptions.Unauthenticated as exc:
        raise ValueError("Access denied") from exc  # Explicit chaining

async def main():
    # After going through retry:
    try:
        await example()
    except ValueError as e:
        print(e.__cause__)  # Expected: "401 Invalid credentials"; Actual: None

if __name__ == "__main__":
    asyncio.run(main())

See https://github.com/googleapis/python-api-core/pull/879/changes for a unit test that fails with the current code and passes with the proposed fix.

Root Cause

In retry_base.py:168, _default_exception_factory() returns (exc_list[-1], None), which causes raise final_exc from None on line 214, explicitly clearing __cause__.

Proposed fix

Preserve the exception's cause attribute:

# Before
return exc_list[-1], None

# After
final_exc = exc_list[-1]
cause = getattr(final_exc, '__cause__', None)
return final_exc, cause

This maintains exception chains for better debugging and meets developer expectations for exception handling.

Draft pull request

#879

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions