Skip to content

# LY001: Exception exc_info

Enforce `exc_info=True` in exception handlers

Aka: "The rule that will save your career at 3 AM"

## Problem

Log exceptions without `exc_info=True` and watch your future self suffer. You've just thrown away your debugging lifeline.

    try:
        result = api.fetch_user(user_id)
    except Exception:
        logger.error("Failed to fetch user")  # ❌ Congratulations, you broke debugging

When this fails in production at 2:47 AM, your log will scream:

    ERROR - Failed to fetch user

And you'll have absolutely no idea: - What exception type actually blew up (ConnectionError? TypeError? A ghost in the machine?) - Where exactly in `api.fetch_user()` things went sideways - What the call stack looked like (your fortune cookie of debugging wisdom) - How deep this rabbit hole goes

## Solution

Add `exc_info=True` and let the healing begin:

    try:
        result = api.fetch_user(user_id)
    except Exception:
        logger.error("Failed to fetch user", exc_info=True)  # ✅ Now you're being smart

Now your logs actually tell a story:

    ERROR - Failed to fetch user
    Traceback (most recent call last):
      File "app.py", line 42, in fetch_user
        response = requests.get(url)
      ...
    ConnectionError: Connection refused

BOOM. You just went from "I have no idea what happened" to "I see exactly what went wrong."

## Impact

Without stack traces (aka "the nightmare path"): - Incident resolution time: 2-4 hours - 3 engineers huddled around a desk, sweating - Someone has to reproduce the issue. In prod. On a Friday at 6 PM. - Career advancement: delayed

With stack traces (aka "the hero path"): - Incident resolution time: 15-30 minutes - One engineer, one terminal, one coffee - Root cause visible in the first glance at logs - You leave the office on time. Your family remembers your name.

## Configuration

    exception_exc_info:
      levels: [error]  # Check error logs only
      severity: fail   # Block CI if violations found

### Options

`levels`: Which log levels to enforce

- `[error]` - Only error logs (recommended)
- `[error, warning]` - Error and warning logs
- `[any]` - All log levels

`severity`: Violation handling

- `fail` - Exit with error (recommended for CI)
- `warn` - Print warning only
- `info` - Informational

## Examples

### ✅ Pass (The Responsible Developer)

    # With exc_info=True - the safe bet
    try:
        connect_db()
    except Exception:
        logger.error("DB connection failed", exc_info=True)  # Gold standard

    # With exception() - Python's built-in shortcut
    try:
        connect_db()
    except Exception:
        logger.exception("DB connection failed")  # Equivalent to exc_info=True

    # Non-exception context - you don't need exc_info here, dummy
    if not user:
        logger.error("User not found")  # Not in an exception handler, so it's fine

### ❌ Fail (The Regret Path)

    # Missing exc_info in exception handler - future you will hate present you
    try:
        process_payment()
    except Exception:
        logger.error("Payment failed")  # 3 AM: "WHY NO STACK TRACE?!?!"

    # Also fails with string formatting - you just moved the error into the message
    try:
        load_config()
    except Exception as e:
        logger.error(f"Config error: {e}")  # You removed the stack trace! For what?!

## When to Use

Always, without exception (pun intended), use in production code: - Exception handlers that log errors (duh) - Background job failures (yes, those too) - API request failures (especially those) - Anywhere an exception happens and you log it

Optional in development: - Exploratory logging (you're debugging locally anyway) - Known/expected exceptions (use your judgment) - Rate-limited error handlers (be reasonable)

## Alternative: logger.exception()

Python's `logger.exception()` is the lazy developer's blessing—it automatically includes `exc_info=True`:

    try:
        risky_operation()
    except Exception:
        logger.exception("Operation failed")  # She's included by default! No questions!

It's equivalent to:

    logger.error("Operation failed", exc_info=True)

Use whichever makes you happy. Both are correct. Pick one and be consistent.

## Real-World Example

From our battle-test analysis of real-world codebases (aka "code that broke in production"):

Celery codebase - Missing exc_info in async backend:

    # celery/backends/asynchronous.py:130
    try:
        self._shutdown.set()
    except RuntimeError as e:
        logging.error(f"Failed to set shutdown event: {e}")  # ❌ Stack trace? Gone. With the wind.

When this breaks at 3:47 AM, you're left with just the exception message, no context. No way to understand where it came from. Just pure chaos.

← Back to Policies - Go read about LY002 while you're at it.