The logging module lesson covered Loggers, Handlers, and Formatters generally. This lesson focuses specifically on what to log ā and what NOT to silently do ā the moment an exception is caught, since that decision determines whether a production incident takes minutes or hours to diagnose.
1exc_info: Preserving the Single Most Useful Piece of Debugging Information
logger.error(str(e)) captures only the exception's message text ā for a ValueError("invalid input"), that's literally just the string "invalid input", with no information whatsoever about *where* in the code that exception originated, what the call chain leading to it looked like, or what line actually raised it. The traceback ā the exact information a developer needs to go from 'something failed' to 'here is the specific line and call path that failed' ā is discarded entirely by this pattern.
logger.error("message", exc_info=True) (or the equivalent, more concise logger.exception("message"), which implies exc_info=True and logs at ERROR level automatically) preserves the full traceback as part of the logged record ā most log formatters and aggregation tools render it as a multi-line block immediately following the log message, exactly as it would appear in an unhandled crash, but now captured and retained even though the exception itself was caught and handled gracefully.
This single practice ā always including the traceback when logging a caught exception, unless you have a specific, deliberate reason not to ā is arguably the highest-leverage logging habit for reducing production incident diagnosis time: the difference between a log line reading "Operation failed: invalid input" and one including the full traceback is frequently the difference between immediately knowing exactly which code path failed versus spending significant time reproducing the failure just to find out.
import logging
logger = logging.getLogger(__name__)
try:
risky_operation()
except Exception as e:
logger.error(str(e)) # BAD: loses the traceback entirely
logger.error("Operation failed", exc_info=True) # GOOD: full traceback preserved
logger.exception("Operation failed") # GOOD: shorthand for exc_info=True, ERROR levelFull traceback captured ā where and how, not just what
2"Log and Swallow": When Logging Hides a Real Problem Instead of Surfacing It
Catching an exception, logging it, and then continuing execution as if the failure hadn't happened ā the 'log and swallow' anti-pattern ā feels responsible (after all, *something* was recorded), but it has a genuine, dangerous consequence: the actual real-world effect of the failure (in the example, a record that was never saved to the database) becomes invisible to everyone and everything except a human actively reading through logs at that exact moment. No error surfaces to the calling code, no alert fires, no monitoring dashboard reflects it ā the failure is real, but its visibility is reduced to 'buried in a log file, if anyone happens to look.'
This is a subtly different problem than the previous section's exc_info issue: even with a perfect, fully-detailed traceback logged, 'log and swallow' still means the *program's actual behavior* proceeds as though the operation succeeded, silently diverging from the caller's expectations. A caller of save_to_database(record) that doesn't know the save silently failed has no opportunity to retry, alert a user, or take any corrective action ā the failure has been logged, technically, but not genuinely *handled* in any meaningful sense.
The correct default, absent a specific, deliberate reason otherwise, is: log the exception with full context AND either re-raise it, convert it to a different exception the caller can meaningfully act on, or return an explicit failure signal (a Result/Success/Failure-style discriminated union, from the Union Types lesson) that the caller must handle. Logging should be one part of a failure response, not a substitute for actually responding to the failure at all.
try:
save_to_database(record)
except Exception as e:
logger.error(f"Failed to save: {e}")
# ...then nothing. The record is silently lost, forever, and
# nobody outside the logs will ever know this failure happened.Record silently lost ā invisible to everyone except a log reader
3Structured Context: Logging Data Fields, Not Just Formatted Sentences
logger.error(f"Payment failed for order {order.id}, user {order.user_id}, amount {order.amount}") puts all the relevant context into one formatted sentence ā readable by a human scanning logs directly, but poorly suited to being searched, filtered, or aggregated programmatically, especially at the scale of a production system generating thousands of log lines per minute across many instances. Finding every failure for a specific user_id means grep-ing (or full-text-searching) for that ID's specific string representation somewhere inside a formatted sentence, hoping the format stays consistent.
extra={"order_id": order.id, "user_id": order.user_id, "amount": order.amount} passed to a logging call attaches those values as actual structured fields on the log record itself (accessible to a custom Formatter, and ā critically ā natively understood by structured logging backends and most log aggregation platforms like Elasticsearch, Datadog, or CloudWatch Logs Insights), rather than embedding them only as substrings inside a formatted message. This makes 'show me every payment failure for this specific user_id, across every service, over the last 24 hours' a direct, reliable field-based query instead of a fragile text search dependent on message-format consistency.
This distinction ā structured fields versus everything crammed into one message string ā mirrors precisely the distinction the Custom Exceptions lesson drew between an exception's message and its structured attributes: in both cases, a human-readable summary remains valuable for quick scanning, but the *actionable, searchable* value comes from attaching the real data as distinct, typed fields a machine can reliably query, not from hoping the right substring happens to be present in a formatted sentence.
logger.error(
"Payment processing failed",
exc_info=True,
extra={"order_id": order.id, "user_id": order.user_id, "amount": order.amount},
)
# In a log aggregation tool: filter/search by order_id, user_id, amount directly
# -- not by grepping through a formatted sentenceQueryable fields, not just text buried in a message
4Step-by-Step Breakdown
except Exception as e: logger.error(str(e)) throws away the single most useful piece of debugging information: the traceback. Let's fix that, and a few other common mistakes.
logger.error(str(e)) throws away the traceback -- exc_info=True (or logger.exception()) preserves it, which is almost always what you actually want.
Checkpoint: What information does logger.error(str(e)) lose, that logger.exception(...) or exc_info=True preserves?
- āThe full traceback ā which function called which, and the exact line where the exception originated
- āThe timestamp of when the error occurred
'Log and swallow' -- catching an exception, logging it, and continuing as if nothing happened -- can hide real bugs from ever being noticed or fixed.
Checkpoint: What is the risk of catching an exception, logging it, and then continuing without taking any other action ("log and swallow")?
- āThe underlying failure (e.g. lost data) becomes invisible to everyone except someone actively reading logs ā it can go unnoticed indefinitely
- āLogging itself adds significant, measurable performance overhead
Structured context (extra=) makes logs searchable and filterable in aggregation tools -- far more useful than everything crammed into one message string.
Logging captures what went wrong; Error Recovery covers what your program should actually DO once it knows.
Preserve a Real Traceback. Finish log_with_traceback(): exc_info=True attaches the full traceback to the log record.
Level Up š
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
Fully supported (via server-side Python execution).
Fully supported (via server-side Python execution).
Fully supported (via server-side Python execution).
Fully supported (via server-side Python execution).
Best Practices
Always include exc_info=True (or use logger.exception()) when logging a caught exception
The traceback is usually the single most valuable piece of information for diagnosing a failure ā omitting it turns a quick diagnosis into a much slower one requiring reproduction of the original failure.
Never log-and-swallow without re-raising, converting, or otherwise surfacing the failure to the caller
A logged-but-silently-continued failure is invisible to everything except someone actively reading logs at that moment ā the caller's code should be able to detect and respond to the failure too, not just the log stream.
Frequent Bugs
Catching an exception, logging only str(e) without exc_info, then discovering during a production incident investigation that the traceback needed to diagnose the root cause was never captured.
Default to logger.exception() (or explicit exc_info=True) for every caught-exception log statement, unless there is a specific, deliberate reason the traceback is not needed.
Real-World Examples
Structured Error Logging in a Background Job Processor
A background job processor handles thousands of jobs per hour, and when a specific job fails, the on-call engineer needs to quickly find every related failure across a specific customer or job type without manually reading through raw log text.
import logging
logger = logging.getLogger(__name__)
def process_job(job):
try:
execute(job)
except Exception:
logger.exception(
"Job processing failed",
extra={"job_id": job.id, "job_type": job.type, "customer_id": job.customer_id},
)
raise # re-raise -- the caller (job queue) needs to know this failed