RizTech Academy logo
RizTech Academy
Writing Django Worth ReadingLesson 4 of 525 min

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

  1. Write try/except: pass around an operation, make it fail, and confirm you get no signal; replace it with handling that logs and informs the user.
  2. Catch a specific exception (a timeout) and let an unexpected one propagate; trigger each and observe the difference.
  3. With DEBUG=True, trigger an unhandled error and read the debug page; set DEBUG=False and confirm users get a generic 500 while the traceback would go to logs.
  4. Add logger = logging.getLogger(__name__) and log at INFO and ERROR; configure LOGGING to write to a file and confirm the entries appear.
  5. Use logger.exception(...) in an except block and confirm the traceback is logged with your message.
  6. Replace a print() debugging statement with a logger.debug(...) and reason about why the logger is better in production.

Official documentation

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