Picture of Björn Geisemeyer
Björn Geisemeyer

Exceptions: strategies and best practices

As the title suggests, in this article I will look at different strategies for dealing with exceptions. Catching or throwing an exception is easy. But how do I proceed sensibly? It is important to consider the Error categories. Each exception can be assigned to an error category and handled accordingly. And then it needs to be clarified who is responsible.

Handling an exception

I am referring to the information already provided in Part 1 example presented:

				
					public class FileProvider
{
	public IEnumerable ReadFileContent(string filename) {
		return File.ReadAllLines(filename);
	}
}
				
			

The signature of the method ReadFileContent does not yet provide the caller of the method with any information about possible exceptions. Its implementation does not know how to handle exceptions. This leads to unexpected results for the callers. The method ReadAllLines on the other hand, does not return any unexpected results. The documentation explains exactly which results are returned. This means that the exceptions defined by this method are transparent to the caller. A solution for these possible results can and should be created. A simple revision of the method ReadFileContent may look like this:

				
					public class FileProvider
{
    public Result<IEnumerable> ReadFileContent(string filename)
    {
        IEnumerable lines = Array.Empty();
        
        try
        {
            lines = File.ReadAllLines(filename);
        }
        catch (Exception exception)
        {
            return new Result<IEnumerable>(false, lines, exception.Message);
        }
            
        return new Result<IEnumerable>(true, lines, string.Empty);
    }
}

public record Result(bool HasValue, T Value, string Message);
				
			

In this revised method, a generic class Result which contains the desired return value. The bool-value indicates whether the value is present, and the Message informs about the corresponding message in the event of an exception. This class is defined as record implemented, which makes them immutable. The try/catchblock ensures that all ten possible results can be handled in this method. Each result is displayed as a corresponding Result is returned. The implementation is simple, the method ReadFileContent receives important added value. It takes responsibility for dealing with all results that may occur and transforms them. It thus implements a use case. It turns the exceptions of a called method into a rule for its own application. This adaptation also changes the signature of the method. Users now know that they can receive more than just one result. The Result returns the desired result as well as possible alternative results. The calling methods can be taken from the Result derive the further workflow via a case differentiation, see [Fig. 3].

Abb. 3 ReadFileContent mit Result FD exceptions best practices
Figure 3

The complexity of the check of the possible results and the return value depends on the use case. It also depends on the use case whether an undesired return is an error and what type of error it is.

Why not simply throw an exception?

A frequent question in training sessions is whether throwing caught exceptions is an acceptable alternative to the handling of exceptions described above. The following listing shows an adapted example.

				
					public IEnumerable ReadFileContent(string filename)
{
    IEnumerable lines = Array.Empty();
    
    try
    {
        lines = File.ReadAllLines(filename);
    }
    catch (Exception exception)
    {
		// Side effect: Maybe log something...
        throw;
    }
        
    return lines;
}
				
			

It represents a classic rethrow-mechanism. The exception is caught in the method, a side effect is executed, for example logging, and finally the same exception is thrown again. In the following, I will explain why this procedure is suboptimal.

Loss of readability: It is not clear from the method signature that, in addition to the lines exceptions can also occur as possible results. It is therefore necessary to create a similarly complex documentation as in [Fig. 1] in the first part of the article in order to inform callers about potential exceptions.

Limited testability: In addition to the expected exceptions, the triggered side effects must also be tested when covering the fail cases. This leads to more complex test cases and requires the use of dummies.

Mixing of responsibilities: According to the Single Responsibility Principle (SRP) has the class FileProvider the responsibility, with the exceptions of the ReadAllLines-method. This does not include triggering a side effect. The class thus assumes an additional responsibility that actually falls within the scope of the calling method. If the information from the exception is required for the rest of the program, the Result-data type from [Listing 2] can be extended accordingly.

Fragmentation of the workflow: Throwing an exception as a result within an application basically means that this exception can be handled at a different level of abstraction. The workflow exits at one point when an exception is thrown and is potentially continued at a completely different point in the program code. This makes it difficult to understand the mapped solution. You have to search in different places and reassemble the overall picture.

Clarification of the error categories: Who is responsible for catching the exception that is thrown? A global exception handler, as described in the "Developer errors" section? This would manage accidental exceptions. However, the exceptions that occur here are known and are partially handled using a side effect. A separate handler would have to be created for these exceptions, which maps a further workflow. This makes it necessary to differentiate between known and unknown exceptions. This in turn requires the creation of separate exception types. Only then can the responsibilities be separated. Finally, the question remains as to whether the side effect is not better stored in the handler for known exceptions. This would relieve the limited testability and the mixing of responsibilities. At the same time, it confirms the fragmentation of the workflow.

When do you use the throw mechanism?

The previous section discusses the situations in which throwing exceptions makes little sense. Now I will describe the places where throwing exceptions makes sense. I will also start with a question that often arises in training courses: Which is better - using your own data types or working with exceptions? The answer to this is simple at first. It depends. As with clarifying errors, the context must also be considered here. A more precise statement can and must be made if the following questions can be answered.

  1. What am I developing?
  2. For whom are exceptions exceptions?
  3. Who is responsible for handling the exceptions?

To clarify these questions, let's look at two examples:

In the first scenario, I am developing a library for PDF generation. A PDF is to be generated from input data and format templates and saved in the file system. This library is to be used as a package in various projects. The interface of this package is its public methods. It also accesses an external resource, the file system.

Operator errors and errors from the environment must be checked. The input data may be invalid for the creation of a PDF. Within the library, I do not know the context from which the data originates. Invalid data is considered an error, as the library cannot fulfill its task with it.

The user is responsible for handling the exceptions generated by this library. The input data is known there. The context in which the PDF is to be created and the associated workflow are also known. Data that leads to invalid conditions for the library's task may be passed to the outside world as an exception. It is important that the exceptions are included in the documentation of the public methods as possible results.

In the second scenario, I am developing a desktop application to create invoices. Among other things, the package from the previous example is used to generate an invoice as a PDF and then send it by email.

The PDF generation package is regarded as an external resource. The exceptions that come from the package can be processed with the data from the context of the application. Ultimately, this leads to an alternative workflow. In this scenario, exceptions are not unexpected, but expected results.

Responsibility for handling the exceptions lies with the desktop application itself. This is where the data is known. The entire accounting workflow is controlled by the application. Possible exceptions are therefore also converted into regular results here.

Conclusion

Exceptions are results in the code that differ from standard return values. Their value lies in logging unexpected behavior in the code. As developers, our task is to decide whether this behavior is actually unexpected in terms of our application. The error categories help with this. Dealing with exceptions is about responsibility. If I am developing an end product, I am responsible for designing solutions for known exceptions. When developing a library that fulfills a subtask, the responsibility for dealing with possible exceptions lies with the user. The strategies presented here are intended to help find suitable solutions.

Our seminars

course
Clean Code Developer Basics

Principles and tests - The seminar is aimed at software developers who are just starting to deal with the topic of software quality. The most important principles and practices of the Clean Code Developer Initiative are taught.

to the seminar "
course
Clean Code Developer Trainer

Conducting seminars as a trainer - This seminar is aimed at software developers who would like to pass on their knowledge of Clean Code Developer principles and practices or Flow Design to others as a trainer.

to the seminar "
course
Clean Code Developer CoWorking

Online CoWorking incl. coaching -
We are often asked what developers can do to keep up with the topic of clean code development. Our answer: Meet up with other Clean Code Developers online on a regular weekly basis.

to the seminar "

Leave a Comment

Your email address will not be published. Required fields are marked *

en_USEnglish