Errors, logging and failing loudly
When something goes wrong in production — and it will — the difference between a five-minute fix and a five-hour investigation is whether your application told you what happened. This lesson is about handling errors honestly (not hiding them) and logging deliberately (so you have a record when you need it). Both are habits beginners skip and regret, because their absence only hurts once you are live and blind.
Do not swallow exceptions
The most damaging error-handling habit is catching an exception and doing nothing useful with it:
# WRONG — the error vanishes, and so does any hope of debugging it
try:
charge_patient(appointment)
except Exception:
pass # silent — you will never know this failed
A bare except: pass (or except Exception: that only logs a vague message and continues) makes failures
invisible. The payment silently did not happen; the report silently was not generated; and there is no
trace. When a user reports "my payment didn't go through", you have nothing to look at. Never swallow an
exception you are not genuinely handling. Either handle it meaningfully (retry, use a fallback, tell the
user) or let it propagate so it is logged and surfaced. Catching everything to make an error "go away" does
not fix the error — it hides it until it costs far more.
Catch narrowly, and only what you can handle
When you do catch, catch the specific exception you expect and can do something about:
from django.core.exceptions import ObjectDoesNotExist
try:
result = external_lab_api.fetch(report_id)
except requests.Timeout: # a specific, expected failure
logger.warning("Lab API timed out for report %s", report_id)
messages.error(request, "The lab system is slow right now — please try again.")
return redirect("report_list")
Catching requests.Timeout specifically means you handle the case you anticipated (the lab API is slow) and
let unexpected errors propagate (a bug in your code should not be silently caught as if it were a
timeout). A broad except Exception catches bugs you did not anticipate and treats them as if you did —
hiding real problems. The rule: catch the narrowest exception that represents a situation you can actually
handle, and let everything else rise.
DEBUG and what users see when it breaks
Django's error behaviour depends on DEBUG (the security-defaults lesson):
- In development (
DEBUG=True), an unhandled exception shows the detailed yellow error page — the traceback, the local variables, the settings. Invaluable for debugging, and a catastrophic information leak in production. - In production (
DEBUG=False), users see a generic 500 page, and the traceback goes to your logs and error-reporting instead. This is correct: users should never see a stack trace (it is confusing and leaks internals), and you should see it (in the logs).
So the traceback does not disappear in production — it moves from the screen to the logs. Which means your
logging had better be set up, or the traceback goes nowhere and you are blind. Provide friendly 404.html
and 500.html templates so the generic pages match your site.
Logging: Python's logging, configured in settings
Django uses Python's standard logging module, configured with a LOGGING dict in settings. The pattern in
your code: get a logger per module and log at the right level:
import logging
logger = logging.getLogger(__name__) # a logger named for the module
def complete_appointment_view(request, pk):
appt = get_object_or_404(Appointment, pk=pk)
logger.info("Completing appointment %s for patient %s", appt.id, appt.patient_id)
try:
appt.complete()
except PaymentError:
logger.exception("Payment failed completing appointment %s", appt.id) # logs the traceback
messages.error(request, "Payment failed. Please try again.")
return redirect("appointment_detail", pk=pk)
Two habits worth forming. Use logger.exception(...) inside an except block — it logs your message
and the full traceback automatically, which is exactly what you want when investigating later. And pass
values as logging arguments ("appointment %s", appt.id), not f-strings — the logging module formats
them lazily (only if the message is actually emitted) and it keeps logs structured. print() is not
logging: it has no levels, no timestamps, no destination control, and it does not appear in production logs.
Use the logger.
The log levels, and using them meaningfully
Logging is only useful if the levels mean something, so you can filter:
| Level | For | Example |
|---|---|---|
DEBUG |
Detailed diagnostic info, dev only | "queryset returned 42 rows" |
INFO |
Normal significant events | "appointment 17 completed" |
WARNING |
Something unexpected but handled | "lab API slow, retrying" |
ERROR |
A failure that needs attention | "payment failed for appointment 17" |
CRITICAL |
The system is broken | "database unreachable" |
Configure production to record INFO and above (or WARNING and above for less noise), and route ERROR+
somewhere you will see it — a file, and ideally an error-tracking service like Sentry, which captures
exceptions with their traceback and context and alerts you. For a real clinic system, an unhandled error you
find out about from Sentry (with the traceback) beats one you find out about from an angry patient. Set
logging up before you deploy, because its whole value is being there when the failure happens.
Check your work
Do not swallow exceptions. except: pass makes failures invisible and undebuggable — either handle
meaningfully or let it propagate to be logged.
Catch narrowly. Catch the specific exception you expect and can handle (requests.Timeout); let
unexpected errors rise rather than hiding bugs behind a broad except Exception.
DEBUG and errors. DEBUG=True shows the traceback on screen (dev only, a leak in production);
DEBUG=False shows a generic 500 and sends the traceback to logs — so logging must be set up.
Logging basics. logging.getLogger(__name__) per module; logger.exception(...) in an except block
logs the traceback; pass values as logging args, not f-strings; print() is not logging.
Levels. DEBUG/INFO/WARNING/ERROR/CRITICAL — used meaningfully so you can filter; route
ERROR+ somewhere you will see it (a file, Sentry) and set it up before deploying.
Practice
- Write
try/except: passaround an operation, make it fail, and confirm you get no signal; replace it with handling that logs and informs the user. - Catch a specific exception (a timeout) and let an unexpected one propagate; trigger each and observe the difference.
- With
DEBUG=True, trigger an unhandled error and read the debug page; setDEBUG=Falseand confirm users get a generic 500 while the traceback would go to logs. - Add
logger = logging.getLogger(__name__)and log atINFOandERROR; configureLOGGINGto write to a file and confirm the entries appear. - Use
logger.exception(...)in anexceptblock and confirm the traceback is logged with your message. - Replace a
print()debugging statement with alogger.debug(...)and reason about why the logger is better in production.
Official documentation
- Django — Logging — The
LOGGINGconfig and Django's loggers. - Django — Error reporting — What happens on errors in production.
- Sentry — Django integration — Capturing exceptions with context.
Next: reviewing Django code.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship