Applications frequently use exceptions for handling validation of data from users. While harmless at first, they come with strong limitations and suppositions about how to perform validation.

1. Activity

Imagine writing a JSON HTTPS API. When receiving a request, you want to validate it.

In Python, the function you have to write is as follows:

Python validate function signature
def validate(data: Dict[str, Any]) -> HttpResponse:
    pass

validate takes a data of type Dict (the parsed JSON object), and returns an object of type HttpResponse.

Let’s now imagine we need the following checks:

  • the key username must be present, and its value must be non-null, and a non-empty string;

  • the key age must be present, and its value must be non-null, and a positive integer;

  • the key gender might be present. If present, it must be a 1-letter long string.

2. Validation with exceptions

We create 1 function per key validation, and we raise an exception upon failure. They are then called from validate.

Exception-based validate_username
def validate_username(data: Dict[str, Any]) -> str:
    username = data.get('username', None)
    if username is None:
        raise Exception("'username' must be provided")
    if not isinstance(username, str):
        raise Exception("'username' must be a string")
    if len(username) == 0:
        raise Exception("'username' must be non empty")
    return username
Exception-based validate_age
def validate_age(data: Dict[str, Any]) -> int:
    age = data.get('age', None)
    if age is None:
        raise Exception("'age' must be provided")
    if not isinstance(age, int):
        raise Exception("'age' must be an integer")
    if age < 0:
        raise Exception("'age' must be positive")
    return age
Exception-based validate_gender
def validate_gender(data: Dict[str, Any]) -> Optional[str]:
    gender = data.get('gender', None)
    if gender is None:
        return None
    if not isinstance(gender, str):
        raise Exception("'gender' must be a string")
    if len(gender) != 1:
        raise Exception("'gender' must be a 1-letter string")
    return gender

With those 3 functions defined, calling them in validate is a matter of wrapping them in a try/except block:

Exception-based validate
def validate(data: Dict[str, Any]) -> 'HttpResponse':
    try:
        username = validate_username(data)
        age = validate_age(data)
        gender = validate_gender(data)
    except Exception as e:
        return HttpResponse(status=400, body=e)

3. Implicit assumptions and User Experience

The previous code works as you would expect. Yet, what happens when the user sends the following data:

Multi-faults user data
{
  "username": "",
  "age": "73"
}

The user would only get 1 error saying that username must be non-empty: validate_age would never be executed, leaving the user to believe there is only 1 error.

The user has to resubmit the data after fixing username. A new error will be sent back about age not being an integer.

Leading to a third submission by the user.

This shows that exceptions do short-circuit the rest of the code during validation, preventing from showing all errors altogether, instead of one after the other.

In the Activity, no requirement impose that errors must be returned one by one instead of all at once.

4. Multiple errors with exceptions

Fixing this with exceptions is not impossible, but turns exceptions into something you must store and manage.

Example of multiple errors handling with exceptions
def validate(data: Dict[str, Any]) -> 'HttpResponse':
    exceptions = []
    try:
        username = validate_username(data)
    except Exception as e:
        exceptions.append(e)

    try:
        age = validate_age(data)
    except Exception as e:
        exceptions.append(e)

    try:
        gender = validate_gender(data)
    except Exception as e:
        exceptions.append(e)

    if len(exceptions) > 0:
        return HttpResponse(status=400, body='\n'.join(exceptions))

5. Conclusion

When used for input validation, exceptions force a sequential handling of error messages. This is their inherent short-circuiting nature.

The example “fix” shows some duplicate code hinting at a better solution. Think about it or see you in another article for more.