Skip to main content

Best practices for exceptions (xtd.core)

Proper exception handling is essential for application reliability. You can intentionally handle expected exceptions to prevent your app from crashing. However, a crashed app is more reliable and diagnosable than an app with undefined behavior.

This article describes best practices for handling and creating exceptions.

Handling exceptions

The following best practices concern how you handle exceptions:

Use try/catch blocks to recover from errors or release resources

For code that can potentially generate an exception, and when your app can recover from that exception, use try/catch blocks around the code. In catch blocks, always order exceptions from the most derived to the least derived. (All exceptions derive from the xtd::exception class. More derived exceptions aren't handled by a catch clause that's preceded by a catch clause for a base exception class.) When your code can't recover from an exception, don't catch that exception. Enable methods further up the call stack to recover if possible.

Don't forget to clean up allocated resources when an exception is raised. Use the RAII idiom for automatic cleanup.

Handle common conditions to avoid exceptions

For conditions that are likely to occur but might trigger an exception, consider handling them in a way that avoids the exception. For example, if you try to close a connection that's already closed, you'll get an invalid_operation_exception. You can avoid that by using an if statement to check the connection state before trying to close it.

if (conn.state() != connection_state::closed) {
conn.close();
}

If you don't check the connection state before closing, you can catch the invalid_operation_exception exception.

try {
conn.close();
} catch (const invalid_operation_exception& ex) {
console::write_line(ex.get_type().full_name());
console::write_line(ex.message());
}

The approach to choose depends on how often you expect the event to occur.

  • Use exception handling if the event doesn't occur often, that is, if the event is truly exceptional and indicates an error, such as an unexpected end-of-file. When you use exception handling, less code is executed in normal conditions.
  • Check for error conditions in code if the event happens routinely and could be considered part of normal execution. When you check for common error conditions, less code is executed because you avoid exceptions.

Notes Up-front checks eliminate exceptions most of the time. However, there can be race conditions where the guarded condition changes between the check and the operation, and in that case, you could still incur an exception.

Call Try* methods to avoid exceptions

If the performance cost of exceptions is prohibitive, some xtd library methods provide alternative forms of error handling. For example, xtd::int32_object::parse throws an xtd::overflow_exception if the value to be parsed is too large to be represented by xtd::int32. However, xtd::int32_object::try_parse doesn't throw this exception. Instead, it returns a boolean and has an out parameter that contains the parsed valid integer upon success. xd::collections::generic::dictionary<key_t,value_t>::try_get_value has similar behavior for attempting to get a value from a dictionary.

Catch cancellation and asynchronous exceptions

It's better to catch xd::operation_canceled_exception instead of xtd::threading::tasks::task_canceled_exception, which derives from xd::operation_canceled_exception, when you call an asynchronous method. Many asynchronous methods throw an xd::operation_canceled_exception exception if cancellation is requested. These exceptions enable execution to be efficiently halted and the callstack to be unwound once a cancellation request is observed.

Design classes so that exceptions can be avoided

A class can provide methods or properties that enable you to avoid making a call that would trigger an exception. For example, the xtd:collections::generic::list class provides methods that help determine the number of items. You can call these methods to avoid the exception that is thrown if you try to access an element outside the range.

The following example shows how to access list itemsd without triggering an exception:

class program {
public:
// Sums up a list of items using the specified initial value without throwing an exception.
static constexpr int accumulate(const list<int>& items, int init_value = 0) noexcept {
int result = init_value;
// Accesses all elements in the element list using the xtd::collections::generic::list::size method to prevent accessing an element outside the list range.
for (xtd::size index = 0; index < items.size(); ++index)
result += items[index];
return result;
}
};

It is also advisable to add the noexcept attribute to the method to guarantee the user that the method will not throw an exception.

Restore state when methods don't complete due to exceptions

Callers should be able to assume that there are no side effects when an exception is thrown from a method. For example, if you have code that transfers money by withdrawing from one account and depositing in another account, and an exception is thrown while executing the deposit, you don't want the withdrawal to remain in effect.

static void transfer_funds(account& from, &ccount& to, decimal amount) noexcept {
from.withdrawal(amount);
// If the deposit fails, the withdrawal shouldn't remain in effect.
to.deposit(amount);
}

The preceding method doesn't directly throw any exceptions. However, you must write the method so that the withdrawal is reversed if the deposit operation fails.

One way to handle this situation is to catch any exceptions thrown by the deposit transaction and roll back the withdrawal.

static void transfer_funds(account& from, account& to, decimal amount) noexcept {
string withdrawal_trx_id = from.withdrawal(amount);
try {
to.deposit(amount);
} catch (...) {
from.rollback_transaction(withdrawal_trx_id);
throw;
}
}

This example illustrates the use of throw to rethrow the original exception, making it easier for callers to see the real cause of the problem without having to examine the inner_exception property. An alternative is to throw a new exception and include the original exception as the inner exception.

static void transfer_funds(account& from, account& to, decimal amount) noexcept {
string withdrawal_trx_id = from.withdrawal(amount);
try {
to.deposit(amount);
} catch (const exception& ex) {
from.rollback_transaction(withdrawal_trx_id);
throw transfer_funds_exception("Withdrawal failed.", ex);
}
}

Capture and rethrow exceptions properly

Once an exception is thrown, part of the information it carries is the stack trace. The stack trace is a list of the method call hierarchy that starts with the method that throws the exception and ends with the method that catches the exception. If you rethrow an exception by specifying the exception in the throw statement, for example, throw e, the stack trace is restarted at the current method and the list of method calls between the original method that threw the exception and the current method is lost. To keep the original stack trace information with the exception, there are two options that depend on where you're rethrowing the exception from:

The following example shows how the xtd::runtime::exception_services::exception_dispatch_info class can be used, and what the output might look like.

auto edi = exception_dispatch_info {};
try {
auto txt = file::read_all_text("C:\\temp\\file.txt");
} catch (const file_not_found_exception& e) {
edi = exception_dispatch_info::capture(e);
}

// ...

console::write_line("I was here.");

if (edi.exception_captured())
edi.rethrow();

If the file in the example code doesn't exist, the following output is produced:

I was here.

Unhandled exception: xtd::io::file_not_found_exception : Unable to find the specified file.
at xtd::io::file::read_all_text [0x0000037C] in C:\Users\yves\Projects\xtd\src\xtd.core\src\xtd\io\file.cpp:line 225
at xtd_console_app::program::main [0x00000080] in C:\Users\yves\Projects\xtd_console_app\xtd_console_app\src\program.cpp:line 12
at xtd::startup::run [0x000000A0] in C:\Users\yves\Projects\xtd\src\xtd.core\src\xtd\startup.cpp:line 157
at xtd::startup::internal_safe_run<void (__cdecl*)(xtd::collections::generic::list<xtd::basic_string<char,std::char_traits<char>,std::allocator<char> >,std::allocator<xtd::basic_string<char,std::char_traits<char>,std::allocator<char> > > > const &)> [0x000000F8] in C:\Users\yves\Projects\xtd\src\xtd.core\include\xtd\startup.h:line 105
at xtd::startup::safe_run [0x0000009C] in C:\Users\yves\Projects\xtd\src\xtd.core\src\xtd\startup.cpp:line 70
at main [0x0000003C] in C:\Users\yves\Projects\xtd_console_app\xtd_console_app\properties\startup.cpp:line 10

Throwing exceptions

The following best practices concern how you throw exceptions:

Use predefined exception types

Introduce a new exception class only when a predefined one doesn't apply. For example:

Notes While it's best to use predefined exception types when possible, you shouldn't raise some reserved exception types, such as xtd::access_violation_exception, xtd::index_out_of_range_exception, and xtd::null_pointer_exception. See exceptions to view all xtd exceptions.

Use exception builder methods

It's common for a class to throw the same exception from different places in its implementation. To avoid excessive code, create a helper method that creates the exception and returns it. For example:

class file_reader {
public:
file_reader(const string& path) noexcept : file_name_ {path} {}

optional<array<byte>> read(int bytes) {
optional<array<byte>> results = file_utils.read_from_file(file_name_, bytes);
if (!results) throw file_reader_exception();
return results;
}

static file_io_exception file_reader_exception() noexcept {
string description = "My file_reader_exception description";
return file_io_exception {description};
}

private:
string file_name_;
};

Some key xtd exception types have such static throw helper methods that allocate and throw the exception. You should call these methods instead of constructing and throwing the corresponding exception type:

If you're implementing an asynchronous method, call xtd::threading::cancellation_token::throw_if_cancellation_requested() instead of checking if cancellation was requested and then constructing and throwing operation_canceled_exception.

Include a localized string message

The error message the user sees is derived from the xtd::exception::message property of the exception that was thrown, and not from the name of the exception class. Typically, you assign a value to the xtd::exception::message property by passing the message string to the message argument of an xtd::exception constructor.

For localized applications, you should provide a localized message string for every exception that your application can throw. You use locale files to provide localized error messages. For information on localizing applications and retrieving localized strings, see the following article:

Use proper grammar

Write clear sentences and include ending punctuation. Each sentence in the string assigned to the xtd::exception::message property should end in a period. For example, "The log table has overflowed." uses correct grammar and punctuation.

Place throw statements well

Place throw statements where the stack trace will be helpful. The stack trace begins at the statement where the exception is thrown and ends at the catch statement that catches the exception.

Don't raise exceptions in noexcept methods

Don't raise exceptions in noexcept methods. For example:

void the_method() noexcept {
throw argument_exception {}; // Error because you specify to the user that the_method has no exception, yet you throw an exception.
}

Throw argument validation exceptions synchronously

In task-returning methods, you should validate arguments and throw any corresponding exceptions, such as xtd::argument_exception and xtd::argument_null_exception, before entering the asynchronous part of the method. Exceptions that are thrown in the asynchronous part of the method are stored in the returned task and don't emerge until, for example, the task is awaited.

Custom exception types

The following best practices concern custom exception types:

End exception class names with _exception

When a custom exception is necessary, name it appropriately and derive it from the xtd::exception class. For example:

class my_file_not_found_exception : public xtd::exception {

};

Include three constructors

Use at least the three common constructors when creating your own exception classes: the parameterless constructor, a constructor that takes a string message, and a constructor that takes a string message and an inner exception.

For an example, see How to: Create user-defined exceptions.

Provide additional properties as needed

Provide additional properties for an exception (in addition to the custom message string) only when there's a programmatic scenario where the additional information is useful. For example, the xtd::io::file_not_found_exception provides the file_name property.

See also