How does a method name communicate failure expectations? When you call a method what do you expect to happen if the method can’t accomplish what it’s meant to?
Will it throw an exception? Will it return some data that allows you (the caller) to be aware of a problem?
It’s a tough situation as a developer, your boss, your teammates, the internet all have their opinions and best practices to bestow upon you, like the following:
- Exceptions are heavy don’t overuse them
- Handle exceptions gracefully
- Expect failures and handle them
None of this advice is wrong. It will however lead you down a frustrating path if they are not followed when and where appropriate. Above and beyond all of that; the most important thing you can do to prevent failures is:
- Effectively communicate expectations to the caller of the method you design
- Effectively communicate expectations of the method to the future developers that need to maintain, use, or extend the functionality of this method
Without effective communication about the expectations of a method, code quickly turns into spaghetti code trying to handle a bunch of oddball scenarios that cause failures and bugs. Which in turn will complicate the ability of the GUI to articulate these exceptions and errors to its end users. The problems surfaced to the end users may have easy work arounds, or problems that are just total failures, and the user should go away and come back later. Effectively communicating failure, possible remediation, and total failure to your end user can have a huge impact on how positive or negative the user experience is. Which is the ONLY REASON we write code to begin with: to positively impact an end user or stakeholder.
In C# when we call a method we usually want to understand the input and outputs as quickly as possible. This way we understand what we need to pass in and what we can expect to happen from calling a method with a given set of parameters.
When I’m choosing the name of my methods I usually name them in two ways:
- A name that communicates the expectation that the method will succeed when called, or an exception will be thrown.
- A name that communicates the expectation that the method will attempt an operation and the caller will need to perform some evaluation to determine if the call was a success. These methods will not throw exceptions, and usually would have a very tight focus on a single operation. An example would be int.TryParse the goal is to parse a string to an int, if it fails to do so it returns a false value indicating the string was not successfully parsed, and you now have to evaluate that boolean value of false to determine what you want to do next.
int.TryParse is an example of a method in .NET that communicates to the caller that it will try an operation and return with a result that indicates if it was successful. When you call methods in the .NET framework you expect them to either throw an exception or complete successfully, such as int.Parse. These two methods illustrate both categories above.
When a developer needs to maintain these functions it’s clear that one throws exceptions, and one returns an indicator of failure (the returned boolean). So the developer should know that he needs to maintain those expectations.
Where we get into trouble is when we do something like this:
Person p = datalayer.AddPerson(firstName, lastName);
Do you expect that line of code to throw an exception if it fails to add the person to the database?
Do you expect p == null if that fails?
Do you expect p != null && p.Id == 0 ?
This is the type of ambiguity we find ourselves in when we deviate from the pattern of throwing or bubbling up exceptions to whatever frame of the call stack has the best chance to remedy the error. In some cases you can’t remedy the problem and you just have to tell the user to try again later.
However when the problem can be remedied, we need a way to know it failed. Exceptions are a consistent contract for understanding WHAT failed. If we go back to the TryParse example, we find that when try parse fails we know it fails but we don’t know why. Which may be ok because parsing an integer is so trivial that we don’t care why, we just know we need to ask the caller for a better value to parse. In our business logic these scenarios are usually more complex with more dependencies and distributed infrastructure involved.
If you call AddPerson and you get a Connection exception because you can’t connect to the database, then that should not be caught in AddPerson but let it bubble up to whatever layer should handle it. In the case of a Connection exception, let’s assume the configuration file is just configured with the wrong connection. If this is executing inside a rest API the expectation of that caller would be a 500, and internally in our code the exception would bubble up to the Action “essentially the application layer”, and in that layer it would be caught, logged, and return the appropriate 500 with a generic error message indicate the server has an issue and to try again later.
The consumer of this api then doesn’t have to make any guesses about the failure, and whether or not there was a problem with their request. If AddPerson returned a 200, with some sort of error message in the payload the user would need to decipher the error message. Once they sort out system errors from errors the user can affect they then need to present them back to the end user in a meaningful way to keep the user experience pleasant.
Surely there are valid use cases for such a contract but it creates complexity because there is more opportunity for you to create an inconsistent response and failure result between different types of operations.
Across boundaries like a call from one system to another system’s REST API there is always a little bit of a need to understand the various exceptions and response codes and translate those back to your use case and UX. However inside a system the various calling methods and called methods should rely on a much simpler and consistent contact, which is what I’m communicating above. Try methods, and methods that throw exceptions.
As usual I’m just trying to lay out some best practices. Everything is situational and quite frankly I’m going to probably put you to sleep if I try any harder to explain this.