Design discussion: when to prefer Result over exceptions (and vice versa)
See original GitHub issue(Without intending any offense:) The code example in the README without result
is kinda braindead:
def get_user_by_email(email: str) -> Tuple[Optional[User], Optional[str]]: """ Return the user instance or an error message. """ if not user_exists(email): return None, 'User does not exist' if not user_active(email): return None, 'User is inactive' user = get_user(email) return user, None user, reason = get_user_by_email('ueli@example.com') if user is None: raise RuntimeError('Could not fetch user: %s' % reason) else: do_something(user)
While a lot of Python code looks like this, this isn’t how one should write Python code. More pythonic would probably be to raise Errors already in the get_user_by_email
function and handle them in the calling code:
def get_user_by_email(email: str) -> Tuple[Optional[User], Optional[str]]:
"""
Return the user instance or raise an error.
"""
if not user_exists(email):
raise KeyError('User does not exist') # or maybe a custom error class
if not user_active(email):
warnings.warn('User is inactive') # Probably shouldn't always be an error,
# should it?
user = get_user(email)
return user
try:
get_user_by_email('ueli@example.com')
except Error as e:
raise RuntimeError('Could not fetch user') from e
else:
do_something(user)
So, it’d be good if the README could advise:
When should one prefer Python Errors over result
and when use a result
rather than an Error?
Issue Analytics
- State:
- Created 3 years ago
- Reactions:4
- Comments:6 (2 by maintainers)
Top Results From Across the Web
Is there any reason to prefer the AIC or BIC over the other?
Usually, the results point to the fact that AIC is too liberal and still frequently prefers a more complex, wrong model over a...
Read more >Prefer composition over inheritance? - Stack Overflow
Prefer composition over inheritance as it is more malleable / easy to modify later, but do not use a compose-always approach. With composition,...
Read more >Exceptions: The Right Way - Software Design & Development
Prefer specific than generic exceptions · Catch the more specific exceptions first · Use clear messages · Document your exceptions · Wrap exceptions....
Read more >Best practices for REST API design - Stack Overflow Blog
Learn how to design REST APIs to be easy to understand for anyone, future-proof, secure, and fast since they serve data to clients...
Read more >Web Platform Design Principles - W3C
This document contains a set of design principles to be used when designing web platform technologies. These principles have been collected ...
Read more >
Top Related Medium Post
No results found
Top Related StackOverflow Question
No results found
Troubleshoot Live Code
Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free
Top Related Reddit Thread
No results found
Top Related Hackernoon Post
No results found
Top Related Tweet
No results found
Top Related Dev.to Post
No results found
Top Related Hashnode Post
No results found
Thanks for pointing out that the docs don’t do a good job explaining things well. Good to know so we can improve them moving forward.
Based on what I understand, pythonic approach is obviously,
EAFP,
But the argument can be made that when you’re not catching an error, you’re leaving someone else to take care of it. It’s implicitly transferring control to someone up the call stack who may not, or even should not, have to care about it. Normally, this is fine, but in exceptional cases, someone far far away from the actual source of the error is forced to handle an error they’re perhaps not meant to handle because an unexpected error wasn’t explicitly handled or similar.
So, what’s the alternative approach? Well one approach is to make it so that the returned value from a function isn’t a value, but a wrapper containing a potential failure. So if you know that a API call can fail for various different reasons, maybe you can, instead of returning null or throwing an exception, return a union type
Ok(value_you_want) | Error(possible_errors_that_can_occur)
, wherepossible_errors_that_can_occur = invalid_argument | item_not_found | networking_error
. With this, you at least make it explicit to the caller that a network error may occur. Then they just pattern match (if
check the error value) to see if an error was returned instead of being forced to catch aHTTP 500 error
from some deep call to the networking library.Obviously, it’s not very pythonic to do it this way, but it the approach you’re seeing more and more in newer languages such as Zig, Rust (which this package is based on) as well as basically all(? or at least a lot) of the functional programming languages, such Haskell, Elm, etc.
If someone disagrees with what I’ve said or can provide better insight than me, please do share.
From my point of view: Returning a result forces you to handle (or ignore) errors. Raising an exception is very easy to miss. But of course they’re both valid approaches to error handling.
The second aspect is that results put the error object in the type signature, while exceptions don’t show up. This is of course only relevant when using mypy. And to be honest, I think
result
mostly makes sense when combined with type annotations. Without type annotations, the benefit is not that big.The discussion is a bit similar to checked vs unchecked exceptions in Java, although that discussion is also influenced by the API design choices that Java made. There are valid reasons for both approaches.
I think results should not be used as a general replacement for exceptions. Exceptional things should probably still be exceptions. Instead, I’d use them when a function is expected to return an error often or when it’s important that errors are handled by the caller (e.g. across module boundaries).
In a complex codebase, I prefer a signature like this:
…over a signature like this:
Does that make sense?