lookaside
lookaside

Reputation: 347

When should I declare custom exceptions?

I want to raise exceptions that communicate some message and a value related to the error. I'm wondering when it's most appropriate to declare custom exceptions versus using the built-ins.

I've seen many examples like this and many more like it being recommended on other sites.

class NameTooShortError(ValueError):
    pass

def validate(name):
    if len(name) < 10:
        raise NameTooShortError(name)

I am much more inclined to write code such as:

def validate(name):
    if len(name) < 10:
        raise ValueError(f"Name too short: {name}")

My instinct would be to only declare custom exceptions if complex or specific information needs to be stored in exception instances. Declaring empty classes seems wrong to me.

Upvotes: 13

Views: 6396

Answers (3)

Jeyekomon
Jeyekomon

Reputation: 3391

There are two questions merged into one: How often should I use custom exceptions (to not overuse them)? and Should I actually ever prefer custom exceptions (to the builtin ones)? Let's answer both.

Custom exception overusing

The blog post from Dan Bader you linked is a great example of how it should not be done. An example of overusing of custom exceptions. Every exception class should cover a group of related uses (ConfigError, BrowserError, DateParserError). You definitely shouldn't create a new custom exception for every particular situation where something needs to be raised. That's what exception messages are for.

Custom vs. builtin exceptions

This is a more opinion-based topic and it also highly depends on the particular code scenario. I will show two interesting examples (out of possibly many) where I consider using a custom exception can be beneficial.

01: Internals exposure

Let's create a simple web browser module (a thin wrapper around the Requests package):

import requests

def get(url):
    return requests.get(url)

Now imagine you want to use your new web browser module in several modules across your package. In some of them you want to catch some possible network related exceptions:

import browser
import requests

try:
    browser.get(url)
except requests.RequestException:
    pass

The downside of this solution is that you have to import the requests package in every module just to catch an exception. Also you are exposing the internals of the browser module. If you ever decide to change the underlying HTTP library from Requests to something else, you will have to modify all the modules where you were catching the exception. An alternative to catch some general Exception is also discouraged.


If you create a custom exception in your web browser module:

import requests

class RequestException(requests.RequestException):
    pass

def get(url):
    try:
        return requests.get(url)
    except requests.RequestException:
        raise RequestException

then all your modules will now avoid having the disadvantages described above:

import browser

try:
    browser.get(url)
except browser.RequestException:
    pass

Notice that this is also exactly the approach used in the Requests package itself - it defines its own RequestException class so you don't have to import the underlying urllib package in your web browser module just to catch the exception it raises.

02: Error shadowing

Custom exceptions are not just about making code more nice. Look at (a slightly modified version of) your code to see something really evil:

def validate(name, value):
    if len(name) < int(value):
        raise ValueError(f"Name too short: {name}")

    return name

Now someone will use your code but instead of propagating your exception in case of a short name he would rather catch it and provide a default name:

name = 'Thomas Jefferson'

try:
    username = validate(name, '1O')
except ValueError:
    username = 'default user'

The code looks good, doesn't it? Now watch this: If you change the name variable to literally any string, the username variable will always be set to 'default user'. If you defined and raised a custom exception ValidationError, this would not have happened.

Upvotes: 12

deceze
deceze

Reputation: 521994

Creating custom exception classes…

  • gives you a declarative inventory of all the expected errors your program may produce; can make maintenance a lot easier

  • allows you to catch specific exceptions selectively, especially if you establish a useful hierarchy of them:

    class ValidationError(ValueError):
        pass
    
    class NameTooShortError(ValidationError):
        pass
    
    ...
    
    class DatabaseError(RuntimeError):
        pass
    
    class DatabaseWriteError(DatabaseError):
        pass
    
  • allows you to separate presentation from code better: The message you put into the exception is not necessarily the message the end user will see, especially if you localise your app into multiple languages. With custom classes, you can write your frontend something like this (using generic common HTML template syntax, _() is the gettext localisation function):

    {% if isinstance(e, NameTooShortError) %}
      <p>{{ _('Entered name is too short, enter at least %d characters') % e.min_length }}</p>
    {% elif isinstance(...) %}
      ...
    {% else %}
      {# fallback for unexpected exceptions #}
      <p>{{ _('An error occurred: %s') % e }}</p>
    {% endif %}
    

    Try that with just ValueError(f'Name too short: {name}')

Upvotes: 2

Shane
Shane

Reputation: 82

Ben, Deciding when to declare custom exceptions is a personal call. Personally I like to use them when I get an error and I stare at the screen scratching my head wondering what exactly it means or in the case of something broad. In the example you gave I would personally put it as "Name too short: {name} please enter a name greater than 10 characters" Just something so that the end user who may not exactly know the needed length would be able to understand why they are getting such an error. Hope this helped :)

Upvotes: 2

Related Questions