Clean code story — Exceptions handling
Why not returning null? And how to handle exception in a clean way
What is the problem with returning null, and allowing variables to be nullable?
It’s not a big deal at first look, but that mean you should always testing if your variables are not null before accessing their properties or methods. So you have some code looking like the following :
This code check if a command is sent, by checking if all items that compose this command are sent. As object instances can be null, we should check it before accessing to the “item.IsSend” property.
Worse, this code return true if a command, or command items are null and don’t return false if an item is null. That a big headache to find how to handle the null value in theses cases. What does a null command mean? How should I handle the fact that a null command is sent? What if an item inside this command is null? How to validate that this null item is sent or not?
And it’s just one problem inside too many. Your code will repeat all these null check every times you need to access properties or method of an instance, contaminating your code with many condition that have nothing to do with your business or your feature.
How to remove nullable variables?
It’s pretty simple, doesn’t allow your code to return null! And Consider your class/struct properties should have a default value, or value should be given by the constructor.
If your code never return a null instance, and if all your instance are correctly initialized, you can’t encouter a null reference. The only limit to it is when you interact with some external services, like a DTO from a front client received by your API, or an external API, that could have some null properties. You should create an ACL (Anti Corruption Layer), to transform the DTO or the API responses into your object, checking for null value, and mapping null value to the default value of this property.
For exemple, if you have an enum:
You can simply add an “unknown” value, to handle the case where you don’t know the correct status.
What nullable have to do with exception ?
In some case, a default value, like the unknown CommandStatus could be a good option, in some other case, you may don’t want to deal with an unknown status. Considering that it should never happened. Because in your business case, you will never have to treat a command with this kind of status, and you can’t guess what to do with it. Yes, exactly like if it was a null value !
So, if a default value can’t give you a good state for your business, you should throw exception, when the problem occurs. For exemple, in your ACL, when a value of the external API cannot match to your current needs, you should throw an exception. This will ensure that your domain never have nullable variable, and ensure that you integrate only data you can work on.
ACL off course arn’t the only part of code that could raise exception. Every part of your code could. Every time your code meet a case where you don’t know what to do with this, you should raise exception.
Before, I was afraidof raising Exception
I don’t know if you have the same feeling, the same memory (if your are a more senior developer), but for me, when I started to code, raise an exception meaned “I have a error” ! Like I want to be a good programmer, I don’t want to have error… So I prefered returning null. Then the code will throw an exception somewhere, saying “Null reference exception”. The stacktrace give me information on where the null reference occurs, but I don’t undestand “How can I have a null value here??”.
To avoid this problem, you should raise an exception when the problem occurs. Instead of a null reference exception, I should have a nammed exception, with the stacktrace showing me the real problem, and even more, some information about the context on when/why it happened.
With these informations, I can understand the real problem, and apply a good solution to handle it. Or may be, simply ignoring it, if this exception is raised by some data the business can’t ingest.
I learned that raising exception doesn’t mean “I have a error”, but it mean “The system cannot handle this case for now”. It’s not my fault as a developer, it’s just that business, system, don’t know what to do in this case. So it’s better to stop the code execution now, than continue to execute code in invalid state.
Custom exception
Always raise custom exception, except if an existing exception match well your case, like ArgumentNullException, ArgumentOutOfRangeException, ect.
By using custom exception, you get some advantages:
- You can find faster where it occurs, because the name of your exception is dedicated to some use case only
- You can get a name that match the reason of the error, offering some possibility for thoses who will handle it to know how to handle it !
- You can give the context information you need to analyze and understand why the exception occurs
Handling exception
There are some good practice, bad practice, and some practice to adapt to your need.
Start with the bad practice. Catching exception, and throwing.
Why it’s bad? The problem with this kind of code is that you will log your exception many times. Because if another method, calling this one, do the same kind of try/catch, the exception will be logged two times. And each time a parent caller do the same, you will log the exception again. That will make difficult for you to analyze your logs.
An another practice is to catch, then throw a custom exception:
This case is opened to be discussed, because I think in some case, it could make sense.
- Why it could be OK? It’s ok if you can hide a system exception, like HTTP exception, to replace it by a domain exception, making more sense to the developer, and hiding the implementation detail of an interface. But you should always ensure that your custom exception take trace of the real exception (as InnerException in C# for exemple).
- Why It could be bad? It’s bad if you catch custom exception, to re-throw custom exception, you’ll lost the original meaning of the exception. You can off course add the original exception to the new one, but why should you have two custom exception, for the same source of error?
As we saw, catching for logging or re-trhow is not a really good option. So when should you catch an exception? The response is simple:
“Catch an exception only when you can handle it and take decision about it”
Exemple 1
What does that mean? You should handle an exception only if you can change the way your application behave when you handle it. For exemple, in an web application, inside a controller, you should catch the potential exception to return a 500 http error status code, instead of a status 200. As you see, you make decision about the error, if an error occurs, the way the application behaves change. In case you have no error, you return status 200, otherwise you return a status 500.
Exemple 2
An other exemple is when you can make a re-try tentative. Imagine you have an HttpTimeOutException, that means the distant server take too long time to respond, or your request has been too long to be sent. But we can imagine that if we try again, it’ll success. The server doesn’t return a 500 error, so retrying may be OK. To do this, we need to catch the HttpTimeOutException, and apply the comportement to retry. Once again, we add some value to fact of this exception occurs.
Exemple 3
A last exemple is when you can try something else to achieve your goal. Imagine you try to access a data in some cache space. But the cache is expired, so, as it can’t return null, it throw an exception like “DataNotFoundException”. You can catch it, and try to get the data from the original database for exemple. This kind of comportement is call “circuit breaker”, even if this exemple is simple.
If you have a retry policy or a circuit breaker policy, catch only the last exception that occurs, including the originals exceptions as InnerException.
These three exemple arn’t an exhaustive list. The global idea is to catch when you can do something with the exception, when you can adapt the program flow to this problem. And logging is not and adaptation ! Logging is just logging.
Log when you catch if you don’t re-trhow, but don’t consider logging as taking a decision about the exception.
Using Option, Result<T> or Monad
Option, Result, or Monad types represent the possibility of a value being present or absent, success or failure, or any other outcome that can occur during program execution. By embracing these constructs, developers can handle potential errors or absence in a more explicit and controlled manner.
Unlike exceptions that can interrupt program flow and make code harder to reason about, Option, Result, or Monad types encourage a more functional and predictable approach to error handling. They promote code that explicitly handles both success and failure cases, resulting in code that is more resilient and less prone to unexpected errors.
Additionally, these constructs encourage a compositional approach, where operations and transformations can be chained together using functional programming techniques. This enables developers to express complex error-handling logic in a more concise and readable manner, avoiding nested try-catch blocks or error-propagation issues.
By using Option, Result, or Monads instead of relying solely on exceptions, developers can create code that is more modular, testable, and maintainable. These constructs provide a structured way to handle errors and absence, leading to more robust and predictable software systems. However, it’s important to note that the choice between exceptions and these constructs depends on the specific requirements and context of the application, and there may be cases where exceptions remain a suitable choice.
How to choice
Using default values can be a straightforward approach for handling situations where a value may be missing or unavailable. It allows the program to continue execution without interruption, providing a predetermined default value. This approach is suitable when there is a clear and reasonable default value that can be used in such cases. However, it’s essential to ensure that default values are well-defined and properly documented to avoid unexpected behavior or incorrect assumptions.
Exceptions, on the other hand, are commonly used for handling exceptional and unforeseen scenarios that may occur during program execution. They provide a way to propagate and handle errors, allowing for immediate interruption of the normal flow. Exceptions are useful when exceptional circumstances require specialized error handling or when specific actions need to be taken in response to unexpected situations. However, exceptions can introduce complexity and should be used judiciously to avoid excessive code branching, performance issues, and reduced code readability.
Constructs like Option, Result, or Monads offer an alternative approach for handling possible errors, absence of values, or different outcomes. These constructs provide explicit representations of success or failure, allowing for more controlled and predictable handling of different scenarios. They promote code that explicitly deals with both success and failure cases, enhancing code modularity, testability, and maintainability. The compositional nature of these constructs facilitates code readability and reduces error propagation, leading to more robust and reliable software systems.
Ultimately, the choice between default values, exceptions, or constructs like Option, Result, or Monads should be based on careful consideration of the specific requirements, complexity, error-handling needs, and maintainability goals of the software project. It’s important to select the approach that best aligns with the project’s context and helps create code that is clear, reliable, and maintainable in the long run.