Error Handling in Python: Result Class

Photo by David Clode on Unsplash
In a previous post, I presented a C# Result class that represents the outcome of an operation. This class is intended to be used for error handling as an alternative to throwing and handling exceptions. I was introduced to this concept by a post from the Enterprise Craftsmanship blog. I recommend reading the entire post, which is part of a series examining how principles from Functional Programming can be applied to C#.
I thought it would be interesting to implement the Result class in Python, and since Python is dynamically-typed this ended up being much simpler than the C# implementation which required the use of generic types. The entire implementation is given below:
| |
The Result class encapsulates all information relevant to the outcome of an operation. For example, let’s say that we have a Result instance named result. If the operation which result represents failed, result.success will be False and result.error will contain a string detailing why the operation failed. If the operation succeeded, result.success will be True (result.error will be None). If the operation produced any output, this will be stored in result.value (an operation is not required to produce an output). Result objects are not intended to replace exception handling in all scenarios, and the author of the EC blog provides a simple rule to determine when each should be used:
- Use a
Resultobject for expected failures that you know how to handle. - Throw an exception when an unexpected error occurs.
To demonstrate how the Result class should be used, the function decode_auth_token in module app.util.auth validates an access token in JWT format. Please note the highlighted line numbers:
| |
- Lines 11-13: If you call a function that returns a
Resultobject, you should check the value ofresult.failure(orresult.success). I prefer checkingresult.failureto reduce unnecessary indentation.- If the operation failed, you should handle the failure immediately or return the result object upstream until you reach an appropriate place to handle and/or report the failure.
- If the operation was successful and you expect the function to return a value, you can retrieve it by calling
result.value. If no value is expected, (as is the case for thecheck_blacklistfunction) you simply keep executing your current function.
- Lines 16 and 29: To indicate that a function (operation) was successful, the function should return
Result.Ok(). You may have noticed in theResultclass that providing avalueas a parameter is optional. If the successful operation produces a result (e.g.payload['sub']) the client can retrieve it fromresult.value. - Lines 19, 22, 28 In the case of decoding a json web token, we expect exceptions
jwt.ExpiredSignatureErrorandjwt.InvalidTokenErrorto occur and we know how to handle them (Deny the user from performing the requested action and prompt them to re-authenticate). This is the exact use case we defined for theResultclass earlier in this post. To indicate that a function has failed, returnResult.Fail(error)(errorshould be a message explaining why the operation failed).
The Python REPL code below demonstrates how the decode_auth_token function behaves and how to interact with the Result objects that the function returns:
>>> access_token = request.headers.get('Authorization')
>>> result = decode_auth_token(access_token)
>>> result
Result<success=True>
>>> result.success
True
>>> result.value
'570eb73b-b4b4-4c86-b35d-390b47d99bf6'
>>> result.failure
False
>>> result.error
>>> print(result)
[Success]
>>> exit()>>> auth_token_bad = request.headers.get('Authorization')
>>> result = decode_auth_token(auth_token_bad)
>>> result
Result<success=False, message="Invalid token. Please log in again.">
>>> result.success
False
>>> result.value
>>> result.failure
True
>>> result.error
'Invalid token. Please log in again.'
>>> print(result)
[Failure] "Invalid token. Please log in again."
>>> exit()>>> auth_token_expired = request.headers.get('Authorization')
>>> result = decode_auth_token(auth_token_expired)
>>> result
Result<success=False, message="Access token expired. Please log in again.">
>>> result.success
False
>>> result.value
>>> result.failure
True
>>> result.error
'Access token expired. Please log in again.'
>>> print(result)
[Failure] "Access token expired. Please log in again."
>>> exit()I find that code becomes easier to read and digest visually when the Result class is incorporated. It becomes easier to discern what happens when a failure occurs and how the failure is handled, in contrast to a design that favors exception handling as the primary method of error handling.
I have taken the time to explain the Python version of the Result class because it will be referenced frequently in upcoming posts. As always, please give me your feedback or questions in the comments!