A guide to getting started with boost::asio
5. Error handling
The next concept we need to be aware of is error handling. In other words, what happens when our work function throws an exception? Boost::asio gives users two ways to handle this case. The errors are propagated through the handlers to the point where a thread calls a run or poll family of functions. The user can either handle the exception through a try/switch statement or they can opt to receive the exception through an error variable. For more information regarding boost, take a look at Error and Exception Handling. In addition, this Error Handling article covers some more useful points as well.
First, we will look at the exception method.
In this example, we post work to the io_service that causes exceptions over and over. The work object is not destroyed either so the io_service should be kept busy. However, when we run the program, we see it exits. The reason is because the exception propagated through the run function, so the worker threads exited. Since all worker threads exited, the program is done since join_all returns. Immediately we can see how this could lead to problems if we are not careful since worker threads could be taken out one by one until the system has none left.
Now let us take a look at the error variable approach that is also possible.
Uh oh! When we run the program we get a crash. Through debugging, we can see that it is because the exception was not caught. This is because the error variable approach does not convert user exceptions to errors but rather boost::asio exceptions. This is very important to keep in mind! If we are passing our own work through an io_service, we have to keep true to C++ exception programming concepts. If the boost::asio library were to generate an error, it would either come as an exception if no error variable was used or it would be converted to an error variable. Depending on our application, we would choose the one that best fits what we need to do.
To further clarify once again if we are using the io_service for user work, we have to use exception handling if the work can generate exceptions. If we are using the io_service for boost::asio functions only, then we can use exception handling or the error variable as either will do. If we are using the io_service for both boost::asio functions and user work, then we can either use both methods or just the exception handling method, but not only the error variable if the work can generate an exception. That should be pretty straightforward to follow.
Now that we know of this little detail, we have to consider what should happen if an exception is actually generated. What we want to do also depends on the type of application we are developing. In other words, are exceptions system failures or context failures? If they are system failures, then we will want to call the stop member function of the io_service and make sure the work object is destroyed so our program gracefully exits. If exceptions are simply context failures, then we will want to setup the worker thread function to call the run function again so the worker thread does not die. Here is the previous example modified.
Now, when an exception occurs, it is outputted and the worker thread goes back to handling work. When the stop member function is called or the work object is destroyed, the run function no longer blocks as we have seen before, so the loop exits and then the thread finishes up. If we were to use this concept on the exception example, we would see an infinite output of the events since we are always posting new events to the queue. Obviously we would never want to have such a situation occur in a real program.
Most of the errors we will run into from the boost::asio library will come from the actual I/O interfaces such as sockets. We are not quite ready to dive into those yet. There are still more useful features of the boost::asio library we need to get exposed to first.