Error handling for Django applications using email
Tags: django, python
Published: 2021-04-18 05:01:35

When you deploy any application to production, there is always this inherent anxiety - what would happen if there are unhandled exceptions?

When an unhandled exception occurs in your Django application, it leads to an internal server error and returns a 500 error code to the client. Completely avoiding such exceptions is usually not possible.

What you can do though is have eyes on your application. A mechanism to get alerted whenever such an exception occurs so that you can take action immediately. 

There are several ways to do this. You could use a tool like Sentry which gives you a very comprehensive stack trace along with the offending request that caused the error. 

However, the simplest way to do this is to send an email to yourself with the stack trace of the exception.

Fortunately, Django already thought of this and has a pretty straightforward mechanism to email yourself with exceptions using the ADMINS setting. Whenever an error occurs, if DEBUG is set to False, Django will email all the IDs mentioned in ADMINS.

## Format of the error email

Before we get into what settings need to be configured to send the email, let us look at what the email looks like.

I am currently building an app called DailyHabits and I will emulate an error by adding a line of code that divides a number by zero.

This will make sure that the server throws an error when this code is invoked. I will then fire an API call that invokes this particular code and and receive an email that contains this in the body:

    Traceback (most recent call last):
      File "/Users/sankalpjonna/habbit_tracker/venv/lib/python3.8/site-packages/django/core/handlers/exception.py", line 47, in inner
        response = get_response(request)
      File "/Users/sankalpjonna/habbit_tracker/venv/lib/python3.8/site-packages/django/core/handlers/base.py", line 179, in _get_response
        response = wrapped_callback(request, *callback_args, **callback_kwargs)
      File "/Users/sankalpjonna/habbit_tracker/venv/lib/python3.8/site-packages/django/views/decorators/csrf.py", line 54, in wrapped_view
        return view_func(*args, **kwargs)
      File "/Users/sankalpjonna/habbit_tracker/venv/lib/python3.8/site-packages/rest_framework/viewsets.py", line 125, in view
        return self.dispatch(request, *args, **kwargs)
      File "/Users/sankalpjonna/habbit_tracker/venv/lib/python3.8/site-packages/rest_framework/views.py", line 509, in dispatch
        response = self.handle_exception(exc)
      File "/Users/sankalpjonna/habbit_tracker/venv/lib/python3.8/site-packages/rest_framework/views.py", line 469, in handle_exception
        self.raise_uncaught_exception(exc)
      File "/Users/sankalpjonna/habbit_tracker/venv/lib/python3.8/site-packages/rest_framework/views.py", line 480, in raise_uncaught_exception
        raise exc
      File "/Users/sankalpjonna/habbit_tracker/venv/lib/python3.8/site-packages/rest_framework/views.py", line 506, in dispatch
        response = handler(request, *args, **kwargs)
      File "/Users/sankalpjonna/habbit_tracker/venv/lib/python3.8/site-packages/rest_framework/mixins.py", line 19, in create
        self.perform_create(serializer)
      File "/Users/sankalpjonna/habbit_tracker/venv/lib/python3.8/site-packages/rest_framework/mixins.py", line 24, in perform_create
        serializer.save()
      File "/Users/sankalpjonna/habbit_tracker/venv/lib/python3.8/site-packages/rest_framework/serializers.py", line 205, in save
        self.instance = self.create(validated_data)
      File "/Users/sankalpjonna/habbit_tracker/habbits/serializers.py", line 28, in create
        print(100/0)

As you can see, the email contains a full stack trace of the exception which you can use to find the exact line in your codebase which is causing it.

This is the simplest form of error handling and is quite nifty because you most likely receive email notifications on your phone and therefore you will be immediately notified when there is an error in your application.

You now have eyes on your application's health.

## Using gmail to send error emails

In order to send the email, there are a few settings that you need to configure in settings.py. You can send the emails using your own gmail account by setting up the smtp credentials as shown below.

    ADMINS = [
        ('Your name', 'your-alerts-email@provider.com'),
    ]
    
    EMAIL_USE_TLS = True
    EMAIL_PORT = 587
    EMAIL_HOST = 'smtp.gmail.com'
    EMAIL_HOST_USER = 'yourmail@gmail.com'
    EMAIL_HOST_PASSWORD = 'gmail-password'

You can get more details about this in the [Django error reporting section](https://docs.djangoproject.com/en/3.1/howto/error-reporting/)

However, for this to work you would have to allow gmail to let you access it via a non gmail app. You can do this in the security settings.

Click on your account profile on top right -> Manage your Google Account -> Security -> Less secure app access -> Turn on access

However, this method is not recommended. It leads to security vulnerabilities, not to mention the fact that you have your gmail account password somewhere in your codebase.

You can protect the password by not using it directly in your codebase and setting it up in the environment variables instead. 

You can further strengthen security by creating a new gmail account whose sole purpose will be for sending error emails. This way, even if this account gets compromised it won't really affect you much. It is more of a burner email.

If you are wondering if it’s worth going through so many hoops and leave yourself open to security issues, you are right. Which is why I recommend using mailgun instead of your personal gmail account to send error emails.

## Using mailgun to send error emails

I recommend mailgun for three reasons:

1. They have comprehensive APIs that let you send all kinds of emails.
2. You can send upto 6k emails a month in their free plan.
3. It is relatively easy to set up.

To send emails using mailgun, signup for an account on mailgun, and create a new domain. I have already set up a domain called dailyhabits.xyz which I own.

The process will involved setting up a few DNS records in my domain provider to prove to mailgun that I own the domain.

Once this process is complete, you can create a new SMTP user for sending error emails.

You can do so by going to Domain settings -> Add new SMTP user in the mailgun dashboard.

After a credential is created, you will be allowed to copy the password to the clipboard post which the password will no longer be available to you unless you explicitly reset it. 

Make sure you copy the password and save it somewhere.

The smtp credentials to configure in settings.py will be as follows:

    ADMINS = [
        ('Server Alerts', 'hello@dailyhabits.xyz'),
    ]
    
    EMAIL_USE_TLS = True
    EMAIL_PORT = 587
    EMAIL_HOST = 'smtp.mailgun.org'
    EMAIL_HOST_USER = 'server-alerts@mg.dailyhabits.xyz	'
    EMAIL_HOST_PASSWORD = '---password copied earlier from mailgun-----'

And that’s it. We are all set up.

## Closing notes

Error handling requires two things.

1. Getting notified when there is an error in your server.
2. A way to trace the line of code which is causing the error.

Both of these requirements get solved with this simple mechanism of sending an email to yourself.

It functions as a notification that is hard to miss since email notifications are received on the phone and the stack trace given inside the email will help you find the offending line of code quite easily.

You can now rest easy knowing that your application is performing well and in case it is not, you will find out immediately.

[Originally posted on my blog](https://www.sankalpjonna.com/learn-django/simple-error-handling-for-django-applications-using-email-notifications)