The Boost C++ Libraries


Chapter 15: Error Handling


Table of Contents

This book is licensed under a Creative Commons License.

A new edition of this book is available! It has been published as a print book and can be bought from Barnes and Noble, Amazon and other bookstores. The new edition is up-to-date and based on the Boost C++ Libraries 1.47.0 (released in July 2011). Several chapters have been updated (for example to Boost.Spirit 2.x, Boost.Signals 2 and Boost.Filesystem 3) and many new libraries are covered (for example Boost.CircularBuffer, Boost.Intrusive and Boost.MultiArray). For more information please see the publisher's website XML Press.


15.1 General

Every function having the potential to fail during execution needs a way to communicate this to its caller appropriately. In C++, this is done either by using a return value or by throwing an exception. A return value is usually used if the failure is not really considered exceptional in a common sense. The caller is expected to check the return value and to react accordingly.

Exceptions are used to indicate exceptional conditions that are not considered to happen normally. A good example is the exception of type std::bad_alloc that is thrown if a dynamic memory allocation using new fails. Since memory can usually be reserved without any issue, it would be cumbersome to always have to check the return value.

This chapter introduces two Boost C++ Libraries assisting a developer in leveraging error handling: Boost.System translates operating system specific error codes into platform independent ones. Thanks to Boost.System, functions returning a value based on an operating system type can be designed in a platform-independent way. Boost.Exception allows to add additional information to any exception that can be utilized inside the catch handler accordingly to better react to the exception.


15.2 Boost.System

Boost.System is a small library defining four classes to identify errors. boost::system::error_code is the most basic class and represents operating system specific errors. Since operating systems typically enumerate errors, boost::system::error_code saves an error code in a variable of type int. The following example illustrates how to use this class by accessing the Boost.Asio library.

#include <boost/system/error_code.hpp> 
#include <boost/asio.hpp> 
#include <iostream> 
#include <string> 

int main() 
{ 
  boost::system::error_code ec; 
  std::string hostname = boost::asio::ip::host_name(ec); 
  std::cout << ec.value() << std::endl; 
} 

Boost.Asio offers the free-standing boost::asio::ip::host_name() function that returns the name of the computer the application is executed on.

An object of type boost::system::error_code can be passed as the sole argument to boost::asio::ip::host_name(). If the underlying operating system function fails, this argument contains the corresponding error code. It is also possible to call boost::asio::ip::host_name() without any argument in case the error code is irrelevant.

boost::asio::ip::host_name() was actually broken in Boost 1.36.0 and therefore serves as a perfect example. The function possibly returned an error code even though the underlying operating system function did actually return the name of the computer successfully. Since the issue was resolved in Boost 1.37.0, boost::asio::ip::host_name() can now be used without concerns.

Since an error code is nothing but a numeric value, it can be displayed with the help of the value() method. Since the error code 0 usually means that no error has occurred, the meaning of other values depends on the operating system and should be looked up in the manual accordingly.

Compiled on Windows XP using Visual Studio 2008, the above application repeatedly generated error code 14 (not enough storage available to complete the operation) while using Boost 1.36.0. Even though boost::asio::ip::host_name() successfully determined the name of the computer, error code 14 was reported. This behavior is actually due to a broken implementation of boost::asio::ip::host_name().

Besides value(), boost::system::error_code offers the category() method. This method returns an object of the second class defined in Boost.System: boost::system::category.

Error codes are simply numeric values. While operating system manufacturers such as Microsoft are able to guarantee the uniqueness of system error codes, keeping error codes unique throughout all existing applications is virtually impossible for any application developer. It would require a centralized database filled with error codes by all software developers to avoid reusing the same codes for different scenarios which certainly is impractical. For this reason, error categories exist.

Error codes of type boost::system::error_code always belong to a category that can be retrieved using the category() method. Operating system errors are represented by the predefined object boost::system::system_category.

By calling category(), a reference to the predefined boost::system::system_category is returned. It allows to retrieve specific information about the category such as the name using the name() method which is system in case of the system category.

#include <boost/system/error_code.hpp> 
#include <boost/asio.hpp> 
#include <iostream> 
#include <string> 

int main() 
{ 
  boost::system::error_code ec; 
  std::string hostname = boost::asio::ip::host_name(ec); 
  std::cout << ec.value() << std::endl; 
  std::cout << ec.category().name() << std::endl; 
} 

Errors are uniquely identified by the error code and the error category. Since error codes are only required to be unique within a category, developers should create a new category whenever they want to define error codes specific to an application. This allows for arbitrary error codes that do not interfere with error codes of other developers.

#include <boost/system/error_code.hpp> 
#include <iostream> 
#include <string> 

class application_category : 
  public boost::system::error_category 
{ 
public: 
  const char *name() const { return "application"; } 
  std::string message(int ev) const { return "error message"; } 
}; 

application_category cat; 

int main() 
{ 
  boost::system::error_code ec(14, cat); 
  std::cout << ec.value() << std::endl; 
  std::cout << ec.category().name() << std::endl; 
} 

A new error category is defined by creating a class derived from boost::system::error_category and implementing different methods as required by the interface of the new category. At minimum, the methods name() and message() must be supplied since they are defined as pure virtual in boost::system::error_category. For additional methods, the default behavior can be overridden accordingly if required.

While name() returns the name of the error category, message() is used to retrieve the error description for a particular error code. Unlike the above example, the ev parameter is usually evaluated to return a description based on the error code.

Objects of type of the newly created error category can be used to initialize an error code accordingly. The example defines the error code ec using the new application_category category. Therefore, error code 14 is no longer a system error; its meaning is specified by the developer of the new error category instead.

boost::system::error_code contains a method named default_error_condition() which returns an object of type boost::system::error_condition. The interface of boost::system::error_condition is almost identical to the one of boost::system::error_code. The only difference is the default_error_condition() method which is only provided by boost::system::error_code.

#include <boost/system/error_code.hpp> 
#include <boost/asio.hpp> 
#include <iostream> 
#include <string> 

int main() 
{ 
  boost::system::error_code ec; 
  std::string hostname = boost::asio::ip::host_name(ec); 
  boost::system::error_condition ecnd = ec.default_error_condition(); 
  std::cout << ecnd.value() << std::endl; 
  std::cout << ecnd.category().name() << std::endl; 
} 

boost::system::error_condition is used just like boost::system::error_code. Both the value() and category() method can be called for the boost::system::error_condition object as shown in the above example.

The reason for having two more or less identical classes is fairly simple: While boost::system::error_code is used for platform dependent error codes, boost::system::error_condition is used to access platform independent error codes instead. By calling the default_error_condition() method, a platform dependent error code is translated into a platform independent error code of type boost::system::error_condition.

If the above application is executed, it displays the number 12 and the error category GENERIC. The platform dependent error code 14 has been translated into the platform independent error code 12. Thanks to boost::system::error_condition, the error is always represented by the same number - disregarding of the underlying platform. While Windows reports the error as 14, the same error may be reported as 25 with a different operating system. Using boost::system::error_condition, the error will always be reported as 12.

The last class offered by Boost.System is boost::system::system_error which is derived from std::runtime_error. It can be used to transport an error code of type boost::system::error_code within an exception.

#include <boost/asio.hpp> 
#include <boost/system/system_error.hpp> 
#include <iostream> 

int main() 
{ 
  try 
  { 
    std::cout << boost::asio::ip::host_name() << std::endl; 
  } 
  catch (boost::system::system_error &e) 
  { 
    boost::system::error_code ec = e.code(); 
    std::cerr << ec.value() << std::endl; 
    std::cerr << ec.category().name() << std::endl; 
  } 
} 

The free-standing boost::asio::ip::host_name() function is provided in two versions: One expecting an argument of type boost::system::error_code and one expecting no arguments. The second version will throw an exception of type boost::system::system_error in case of an error. The exception transports the error code of type boost::system::error_code accordingly.


15.3 Boost.Exception

The Boost.Exception library offers a new exception type boost::exception allowing to add information to an exception after it has been thrown. It is defined in boost/exception/exception.hpp. Since Boost.Exception spreads its classes and functions over multiple header files, the following example rather accesses boost/exception/all.hpp to avoid including the header files individually.

#include <boost/exception/all.hpp> 
#include <boost/lexical_cast.hpp> 
#include <boost/shared_array.hpp> 
#include <exception> 
#include <string> 
#include <iostream> 

typedef boost::error_info<struct tag_errmsg, std::string> errmsg_info; 

class allocation_failed : 
  public boost::exception, 
  public std::exception 
{ 
public: 
  allocation_failed(std::size_t size) 
    : what_("allocation of " + boost::lexical_cast<std::string>(size) + " bytes failed") 
  { 
  } 

  virtual const char *what() const throw() 
  { 
    return what_.c_str(); 
  } 

private: 
  std::string what_; 
}; 

boost::shared_array<char> allocate(std::size_t size) 
{ 
  if (size > 1000) 
    throw allocation_failed(size); 
  return boost::shared_array<char>(new char[size]); 
} 

void save_configuration_data() 
{ 
  try 
  { 
    boost::shared_array<char> a = allocate(2000); 
    // saving configuration data ... 
  } 
  catch (boost::exception &e) 
  { 
    e << errmsg_info("saving configuration data failed"); 
    throw; 
  } 
} 

int main() 
{ 
  try 
  { 
    save_configuration_data(); 
  } 
  catch (boost::exception &e) 
  { 
    std::cerr << boost::diagnostic_information(e); 
  } 
} 

The example calls the function save_configuration_data() inside main() which in turn calls the allocate() function. allocate() allocates memory dynamically; however, it checks whether or not a certain limit is exceeded. This limit is arbitrarily set to 1,000 bytes in the example.

If allocate() is called with a value greater than 1,000, an exception is thrown accordingly which is the case inside the save_configuration_data() function. The function supposedly saves configuration data in the dynamically allocated memory as indicated by the comment.

The purpose of the example is to actually throw the exception in order to demonstrate Boost.Exception. The exception, thrown by allocate(), is of type allocation_failed and is derived from both boost::exception and std::exception.

Deriving from std::exception is certainly not necessary. allocation_failed could have also been derived from a different class hierarchy in order to embed it inside an existing framework. While the above example uses the class hierarchy defined by the C++ standard, deriving allocation_failed solely from boost::exception would have also been sufficient.

When throwing an exception of type allocation_failed, the size of the memory expected to be allocated, is stored inside the exception to alleviate debugging of the corresponding application. If more memory is requested than can be provided by allocate(), the root cause for the exception can be easily spotted.

If allocate() is called by only one function (in the example save_configuration_data()), this information is sufficient to locate the issue. However, in more complex applications with many functions calling allocate() to dynamically allocate memory, the information is no longer sufficient to effectively debug the application. In these cases, it rather would help to actually know which function tried to allocate more memory than allocate() can provide. Adding more information to the exception in these cases would help the debugging process tremendously.

The particular challenge is that allocate() does not have information such as the caller name in order to add it to the exception accordingly.

Boost.Exception offers the following solution: Information can be added to an exception at all times by defining a data type based on boost::error_info for every information that should be added to the exception.

boost::error_info is a template expecting two arguments: The first argument is a so-called tag, uniquely identifying the newly created data type. Commonly, a structure with a unique name is utilized. The second argument refers to the data type of the information stored inside the exception.

The application defined a new data type errmsg_info, uniquely identifiable via the tag_errmsg structure, which stores a string of type std::string.

Inside the catch handler of save_configuration_data(), the data type tag_errmsg is accessed to create an object, initialized with the string "saving configuration data failed", to add additional information to the exception of type boost::exception via the operator<<() operator. The exception is then re-thrown accordingly.

The exception now not only carries the size of memory expected to be dynamically allocated, but also the description of the error added inside the save_configuration_data() function. This description certainly helps while debugging since it now becomes clear which function tried to allocate more memory than can be provided.

To retrieve all available information from an exception, the boost::diagnostic_information() function can be used as seen in the example inside the catch handler of main(). For every exception passed, boost::diagnostic_information() not only calls the what() method but also accesses all additional information stored inside the exception. A string of type std::string is returned that can e.g. be written to the standard output stream.

Above application compiled with Visual C++ 2008 displays the following message:

Throw in function (unknown)
Dynamic exception type: class allocation_failed
std::exception::what: allocation of 2000 bytes failed
[struct tag_errmsg *] = saving configuration data failed

As can be seen, the message contains the data type of the exception, the error message as retrieved from the what() method, and the description including the corresponding name of the structure.

boost::diagnostic_information() checks at run-time whether or not a given exception is derived from std::exception. Only if it is, the what() method is actually called.

The name of the function that has thrown the exception of type allocation_failed is indicated as "unknown" in the message.

Boost.Exception provides a macro to throw an exception including not only the name of the function but also additional information such as the file name and the line number.

#include <boost/exception/all.hpp> 
#include <boost/lexical_cast.hpp> 
#include <boost/shared_array.hpp> 
#include <exception> 
#include <string> 
#include <iostream> 

typedef boost::error_info<struct tag_errmsg, std::string> errmsg_info; 

class allocation_failed : 
  public std::exception 
{ 
public: 
  allocation_failed(std::size_t size) 
    : what_("allocation of " + boost::lexical_cast<std::string>(size) + " bytes failed") 
  { 
  } 

  virtual const char *what() const throw() 
  { 
    return what_.c_str(); 
  } 

private: 
  std::string what_; 
}; 

boost::shared_array<char> allocate(std::size_t size) 
{ 
  if (size > 1000) 
    BOOST_THROW_EXCEPTION(allocation_failed(size)); 
  return boost::shared_array<char>(new char[size]); 
} 

void save_configuration_data() 
{ 
  try 
  { 
    boost::shared_array<char> a = allocate(2000); 
    // saving configuration data ... 
  } 
  catch (boost::exception &e) 
  { 
    e << errmsg_info("saving configuration data failed"); 
    throw; 
  } 
} 

int main() 
{ 
  try 
  { 
    save_configuration_data(); 
  } 
  catch (boost::exception &e) 
  { 
    std::cerr << boost::diagnostic_information(e); 
  } 
} 

By using the macro BOOST_THROW_EXCEPTION instead of throw, additional information such as function name, file name and line number are automatically added to the exception. This only works if the compiler supports corresponding macros though. While macros such as __FILE__ and __LINE__ are defined by the C++ standard, there is no standardized macro for returning the name of the current function. Since many of the compiler manufacturers provide such a macro, BOOST_THROW_EXCEPTION tries to identify the underlying compiler and utilize the corresponding macro accordingly. Compiled with Visual C++ 2008, the above application displays the following message:

.\main.cpp(31): Throw in function class boost::shared_array<char> __cdecl allocate(unsigned int)
Dynamic exception type: class boost::exception_detail::clone_impl<struct boost::exception_detail::error_info_injector<class allocation_failed> >
std::exception::what: allocation of 2000 bytes failed
[struct tag_errmsg *] = saving configuration data failed

The code compiles without errors even though the allocation_failed class is no longer derived from boost::exception. BOOST_THROW_EXCEPTION accesses a function named boost::enable_error_info() that dynamically identifies whether or not an exception is derived from boost::exception. If not, it automatically creates a new exception type derived both from the specified type and boost::exception. This mechanism is the reason for the above message to not display allocation_failed only.

Finally, this section concludes with an example to selectively access information added to an exception.

#include <boost/exception/all.hpp> 
#include <boost/lexical_cast.hpp> 
#include <boost/shared_array.hpp> 
#include <exception> 
#include <string> 
#include <iostream> 

typedef boost::error_info<struct tag_errmsg, std::string> errmsg_info; 

class allocation_failed : 
  public std::exception 
{ 
public: 
  allocation_failed(std::size_t size) 
    : what_("allocation of " + boost::lexical_cast<std::string>(size) + " bytes failed") 
  { 
  } 

  virtual const char *what() const throw() 
  { 
    return what_.c_str(); 
  } 

private: 
  std::string what_; 
}; 

boost::shared_array<char> allocate(std::size_t size) 
{ 
  if (size > 1000) 
    BOOST_THROW_EXCEPTION(allocation_failed(size)); 
  return boost::shared_array<char>(new char[size]); 
} 

void save_configuration_data() 
{ 
  try 
  { 
    boost::shared_array<char> a = allocate(2000); 
    // saving configuration data ... 
  } 
  catch (boost::exception &e) 
  { 
    e << errmsg_info("saving configuration data failed"); 
    throw; 
  } 
} 

int main() 
{ 
  try 
  { 
    save_configuration_data(); 
  } 
  catch (boost::exception &e) 
  { 
    std::cerr << *boost::get_error_info<errmsg_info>(e); 
  } 
} 

The example does not utilize boost::diagnostic_information() but rather uses boost::get_error_info() to directly access the error message of type errmsg_info. boost::get_error_info() returns a smart pointer of type boost::shared_ptr. In case the argument passed is not of type boost::exception, a null pointer is returned accordingly. If the BOOST_THROW_EXCEPTION macro is always used to throw an exception, it is guaranteed that the exception is derived from boost::exception - there is no need to check the returned smart pointer for null in these cases.