When designing methods in our solutions, it’s hard to communicate all of the behaviors of the method we are writing. How does the caller of our method know what the behaviors of the method will be?
- Will it throw exceptions?
- Will it block?
- Will it cause side effects?
- Will it retry?
- Will it be idempotent?
My team and I try to adhere to a default set of behavioral assumptions about methods. Let’s also keep in mind that there are at least two contexts under which a method is designed. The first group being methods only intended to be used by their creators (usually private or internal), but of course if you work on a team of developers you won’t be the only one using the methods you write. The other context is a method which we expect others to use and maybe even developers that we don’t know or that work for other companies. In either case we find that we need to have or communicate some basic assumptions about any given method.
This post will only cover how we communicate if a method will throw or not.
Will it throw exceptions?
Exceptions are, and should be exceptional. We believe methods should be written and used with the expectation that they’ll succeed, as a matter of convention. Which is not implying that you should not expect failure. It’s actually implying that any normal method, should succeed at it’s intended operation or throw an Exception if it can’t succeed for an exceptional reason that could be planned for or controlled by the developer. Which communicates to the developer writing the Caller method that the Callee may throw an exception and somewhere in the call chain there should be an exception handler.
Then there are methods where failure is not exceptional, but part of the normal and expected behavior. These methods are the exception to our convention of “methods are expected to succeed or throw.”
This class of methods usually needs to test or attempt something that has a high failure rate, is based on an input that hasn’t yet been sanitized, or requires special handling of failures. An examples of such methods are the TryParse methods found on many primitives, like int.TryParse. These are the methods of Exceptional Behavior, in that failure is not exceptional, and exceptions should not be thrown, because failure is an expected behavior.
Microsoft has published guidance as part of the .NET Framework guidelines regarding two patterns they call the “Tester-Doer Pattern”, and the “Try-Parse Pattern”, you can read more about these patterns here: https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/exceptions-and-performance
The subject of exception handling and designing methods that communicate their behavior when it comes to throwing exception is so near and dear to me, that I have another blog entry similar to this one which can be found here: https://blog.ramp51.com/2021/12/21/method-design-communicating-failures/ which really digs in to the ideas around how you maintain consistent expectations for the consumers of the methods you design when designing both methods of conventional behavior and methods of exceptional behavior.
In a future blog posts we’ll get into another conventions and expectations around methods as they pertain to blocking, reentrancy, idempotency, and purity.