All functions have some purpose for which they are designed; each function is meant to do some particular thing. But functions can sometimes fail to do that, whether through faulty parameters, violations of pre-conditions, or even program-external state which has failed to achieve anything.

Error handling is a very important issue for most C++ programs. When a function is incapable of performing the action that has been requested of it, then the function which invoked that process needs to be informed of this failure. If a function does not report errors, then the function is assumed to be incapable of failing to do what it is intended to do

This paper will attempt to catalog the various ways that errors are handled across the wide variety in the C++ ecosystem. This paper will focus on errors that the user can at least theoretically recover from, rather than those for which termination (with possibly some data sent to an error dump) is the only possible response.

This paper is intended to be a catalog of information about the various error reporting mechanisms. It is not intended to judge one mechanism as being better than any other. It is intended merely to present the facts of how they work under various conditions, so that informed decisions can be made about them.

Purpose of errors

The question of what it means for a function to issue an error is somewhat ambiguous. It is easy to get programmers to agree on some kinds of error conditions. The failure to open a file is an error condition, for example. But other kinds of conditions are more murky. Is failure to parse an error?

For the purpose of this paper, we will assume that an error is a condition that has halted the progress of some function. That function has decided that it cannot carry out, in part or in full, the operation that its caller gave to it. Therefore, it must do two things: transfer control to some other code, and inform the other code that an error occurred.

Some APIs provide a strong error guarantee: if the function reports an error, then it is as if the function were never called. Other APIs do not, leaving the system in a partial or uncertain state.

This question is entirely orthogonal to the matter of how the error is actually handled. As this paper is focused on error handling, rather than the state of the system after generation, this paper will ignore what kind of error guarantees the prospective systems offer. This paper focuses on how the information about the error is conveyed to the user.

Terminology and organization

When talking about errors, it is very important to know which code we are talking about at any particular time. Therefore, the following terminology will be used:

Each of the following error reporting mechanisms will feature an explanation of the concept, followed by a detailed look at the specific consequences of the mechanism. In particular, each mechanism will be examined under the following circumstances:

Survey of error methods

Error code as return value

This is one of the most common forms of error reporting. TargetFunc's return value is designated an error code. CallerFunc can test this code to see if an error took place or not.

Many APIs, particularly C-based ones, use this as their primary means of error reporting.

Local error resolution

Locally resolving returned error code values is fairly simple, syntactically. The return value is tested with an if statement of some sort. A common way to do it is as follows:

if(TargetFunc(...) != <no error>)
{
  //Failure code, perhaps correcting state or returning.
}

//Success or state is otherwise fine.

Of course, this syntax completely ignores the specific nature of TargetFunc's error. For error reports where the resolution requires the error code itself, the code tends to be structured this way:

if((auto error_value = TargetFunc(...)) != <no error>)
{
    //Failure code, testing error_value for the type of error.
}

//Success or state is otherwise fine.

There is an additional syntactic burden beyond the above. Since C++ can only return a single value from a function, the error code takes the place of whatever TargetFunc's return value would have been. For functions that do not conceptually return values, this is fine.

But this imposes a burden for those functions that would like to return a value, however. The value must become an output parameter. For functions that return simple types, this means that CallerFunc must do something like this:

output_type output_value = {};
if((auto error_value = TargetFunc(output_value, ...)) != <no error>)
{
    //Failure code, testing error_value for the type of error.
}

//Success or state is otherwise fine.

This makes the code look a bit awkward. It also prevents auto dediction of TargetFunc's true output.

A more pressing problem is that output_type must be either default constructible or constructible in way that is directly exposed to the user. If the non-move/copy constructors of the class are private, and TargetFunc is creating the class instance for the user (and thus, the error code represents construction failure), then this poses a problem for the interface. In that case, TargetFunc will have to allocate the instance and "return" a pointer to the instance.

Non-local error resolution

Propagating the error code up the call stack imposes a significant syntactic burden. CallerFunc must now have some way of communicating the error itself, typically via the return value. And every function that could potentially propagate this error must also return it.

This becomes more complicated in the case of a function that calls a number of different functions, each of which might error with different error code types. This now requires that CallerFunc must return some form of variant type, with all of the possible error codes lists. Furthermore, any code between CallerFunc and the eventual consumer of the error must be aware of every possible error code type, even if they do not wish to consume that particular type.

Note that this is very much like the C++98/03 problem with throw specifications on function definitions. The use of any could be useful in hiding the type, but this forces significant burden on the eventual resolution code, which must attempt to any_cast it to every possible error code type that it can resolve.

Return values via std::thread are not permitted. However, returning values via std::async or future/promise are quite safe, void of race conditions. Return values of arbitrary types are supported by these mechanisms.

Ignored errors

Handling returned error codes requires more effort than ignoring them. To ignore them, CallerFunc simply does nothing with the return value. This also means that it is very easy for CallerFunc to accidentally ignore the error.

Even when errors are ignored, there remains some minor burden. CallerFunc is ignoring the return value, but it cannot ignore the syntactic consequence of TargetFunc's return value being an error code. This means that it is impossible for CallerFunc to chain calls in one statement, even if it is ignoring the error code. This is of course because the function's true output is a parameter, so it must be handled on a separate line:

return_type output_value = {};
TargetFunc(output_value, ...);
output_value.OtherFunction();

This also has the unfortunate burden of forcing output_value to exist until the termination of the current block. If it were a return value temporary, it could have been destroyed immediately after being used by OtherFunction.

While a simple textual search tool cannot detect where returned error codes are ignored, it is possible to write such a tool based on C++ compiler front-ends like Clang. Compilers, if they know that a particular type is an error code (perhaps via an attribute), can certainly generate warnings if a function does not at least use the value in an expression. This also would allow the user to specify, by convention, that an error is being ignored deliberately:

TargetFunc(...); //Accidental error ignorance.
(TargetFunc(...)); //Used in a parenthesis expression, therefore intensional.

If ignoring the error eventually leads to termination or malfunction due to incoherent internal state, there is generally no way to track the source of that error back to CallerFunc. In some cases, the crash may happen within CallerFunc, but in many cases, it will happen in a distant location.

Error code as output parameter

This is an alternative version of returning an error code value. Instead of using the function's return value, the error code is made into an output parameter. CallerFunc provides a non-const pointer/reference to an error code value, which TargetFunc will fill in.

Boost.Filesystem, and the current FileSystem proposed TS both use this method for many functions (as alternatives to exceptions. These overloads are provided as an alternative to the versions of functions that take exceptions.

Local error resolution

The syntax is slightly more complex than for returned error codes, owing to two facts. First, the error code variable must be declared before it is used. And second, because the return value is not the error code, one cannot put TargetFunc directly into a conditional statement:

error_code ec = {};
TargetFunc(..., ec);
if(ec != <no error>
{
    //Failure code, perhaps correcting state or returning.
}

//Success or state is otherwise fine.

Additional syntactic burdens are placed upon the code. If TargetFunc returns a value, it must always return a value, which must be a live C++ object. Therefore, in the event of an error, the returned object must be in some well-defined-yet-innocuous state. And therefore, any object returned by TargetFunc must support being in such a state.

error_code ec = {};
auto output_value = TargetFunc(..., ec);
if(ec != <no error>)
{
    //Failure code.
    //If not returning, `output_value` may need fixup.
}

//Success or state is otherwise fine.

The use of optional, from the current library fundamentals v1 DTS, can alleviate the requirements placed on TargetFunc's return type. In the event of an error, TargetFunc can simply return nullopt.

Parameter error codes also put a syntactic burden on the error code type itself. It must be constructible by the user. The error code type used in the return value case could be a class who's non-move/copy constructors are all private. That could be done to ensure that only those functions who are authorized to construct them may do so. This cannot be used for parameter error codes.

Non-local error resolution

The issues for non-local resolution are, generally speaking, identical to those for returning error codes. CallerFunc must explicitly propagate the error code, either as a return value or an output parameter, to higher-level code.

Propagating by output parameter is a bit more difficult than by returning the error code directly. This is because a function may have to propagate multiple kinds of errors, which would have to go into some kind of variant type. So the variant would need to have some valid state before CallerFunc put the error code into it. While some variants do have an empty state, not all variant types permit this (at least, not without explicitly declaring one of the states to be empty).

Using output parameters in a thread function or other forms of async dispatch is highly dangerous, for several reasons. Pointers to error code objects have to be used, since it is a rare case of thread dispatch (at least with modern syntax; asyc/await may change this) where a reference to an automatic object can be easily used. Furthermore, access to the referenced memory needs to have some kind of synchronization to avoid data races.

Ignored errors

Unlike returning error codes, CallerFunc must always provide at least some syntax to ignore an error. It must instantiate an error code object and pass a pointer/reference to TargetFunc:

error_code ec = {};
auto output_value = TargetFunc(..., ec);

However, CallerFunc only has to declare one error code for each of the kinds of error codes that the various target functions it calls use. So the syntactic cost, while not negligible, is fairly minimal.

Doing a textual search for the error code type is sufficient to at least find all of the places where functions that provide those errors are being used. This means a simple find-in-files is somewhat sufficient for finding such locations, though a compiler front-end based method will be more robust to aliasing, macros, and so forth.

Because at least some syntax is required to store the error, it is at least marginally more difficult to ignore it by accident.

With this syntax, it is possible to ignore the error while chaining. So this is legal code:

error_code ec = {};
auto output_value2 = TargetFunc(..., ec).OtherFunction();

It would even be legal to pass ec to OtherFunction. The specific error will be lost, but since CallerFunc is ignoring errors anyway, this is irrelevant.

The semantic consequences of ignoring an error reported in this way are equivalent to ignoring error codes returned by TargetFunc.

Special return value

Some data types have values which are considered to be non-legal values. IEEE-754 floating-point numbers have NaN values, which are literally "Not a Number". Pointers can be null, which represents an unusable pointer value.

In some cases, return values could be technically legal for a particular type, but make no sense in a particular context. For example, if a function returns an index as an int, it could return a negative number. If indices are not allowed to be negative, this is a conceptually illegal value, while still legally being a valid int.

Functions can return such special values in lieu of explicit error codes. In most cases, these can only represent simple, binary error codes; either an error happened, or the value is legitimate. In some cases, there can be a range of error states. In the negative integer case, the particular negative number could specify the specific failure mode.

This kind of error reporting is used in some APIs that normally use other error reporting mechanisms. For example, OpenGL generally uses out-of-band error codes. But some OpenGL functions, like glGetUniformLocation, will simply return an illegal value if the named uniform location does not exist in the program. No OpenGL error is generated in this way.

Local error resolution

While it is inviting to attempt to use the same structural syntax as the case of returning error codes, that is not possible here. Because the error code and return value are the same object, the return value will often need to be visible outside of the condition that resolve the error. As such, this case often looks like this:

auto output_value = TargetFunc(...);

if(output_value == <error>)
{
    //Failure code, perhaps correcting state or returning.
}

//Success or state is otherwise fine.

The syntax is fairly minimal, comparable to the syntax for the prior cases.

Non-local error resolution

Because the error value and real return value are the same, propagating the error to other code is very natural. This natural propagation extends only to code that has some interest in TargetFunc's return value. For code where no such interest exists, propagating an error becomes more difficult.

Because code that does not care about the actual value may need to have errors propagated to it, CallerFunc or a similar function in the sequence will have to translate the error state. This improves, to some degree, the issue of having only binary pass/fail error state for many kinds of special values. CallerFunc can translate the error into a proper error code type for consumption by others.

This method works well for most forms of asynchronous dispatch operations, as it relies only on returning values.

Ignored errors

Because the error code and return value are the same, it is highly unlikely that the user will want to simply ignore the value. Thus, this section will consider "ignoring the error" to simply mean not checking to see if it is a valid value.

Syntax-wise, this results in code that looks normal:

auto output_value = TargetFunc(...);
//Do something with `output_value`.

Tracking such syntax statically is fairly difficult, as it requires knowing that a specific return value from a specific function needs to be error tested. And usually, such types are common types like pointers or integers, so it would be difficult for even a syntax-aware tool to automatically find such ignored values.

Semantically, ignoring such errors has potentially created another avenue for problems. Whatever CallerFunc does with the illegal value effectively corrupts any expression it is used in. This is the classic issue of storing a null pointer in a location where everyone expects the pointer to not be null. Other code will access it on the assumption that it is valid when it is not. Otherwise, other code will have to constantly check that it is valid even when it is.

This corruption can have far-ranging implications. Depending on where a variable is set from, it can also be exceedingly difficult to track down where the null pointer came from. Setting memory breakpoints and other debugging tools can help.

Even worse, the range of possible broken behavior is extremely wide. Null pointers tend to be the safest, as they usually crash the program immediately upon their use. Floating-point quiet NaN can be used in any math operation without causing a hard fault. Each expression corrupts everything around it, leading to corrupted state throughout the program. The error spreads without being seen or dealt with.

Some APIs can be robust against such corruption. For example, in the OpenGL function listed above (glGetUniformLocation), failure is purely silent. All API functions that take uniform locations will also take the GL_INVALID_INDEX error value. They will then ignore any calls that pass this index, pretending to do whatever they were asked without actually doing anything.

Out-of-band error codes

Though much less common for simple functions, this error scheme is often used on an library-wide level. Among the most well-known users of this scheme is the OpenGL API, but other APIs like cairo use it as well. The C++ iostreams library uses this means of reporting many kinds of errors (though it can also fire exceptions, if set up properly).

The general idea is that errors are not reported in any way by the failed function. Instead, user code is required to call a specific API, which returns an error code if a previous API function call failed.

Where the out-of-band error is stored depends on the API. In cairo and iostream, it is stored in and retrieved from a C or C++ object. In OpenGL, errors are stored in a more nebulous object called an "OpenGL context".

Local error resolution

Local testing syntax does not directly impinge on TargetFunc's API. CallerFunc does not need to do any special processing of return values or provide any special parameters. Thus, the code looks quite normal:

auto output_value = TargetFunc(...);

if((auto ec = DidError()) != <no error>)
{
    //Failure code, perhaps correcting state or returning.
}

//Success or state is otherwise fine.

A common shorthand technique for dealing with errors, particularly in OpenGL, is to use a macro:

glTargetFunc(...); CHECK_OPENGL_ERRORS();

Where CHECK_OPENGL_ERRORS would be defined as this:

#define CHECK_OPENGL_ERRORS()\
    while((GLenum ec = glGetError()) != GL_NO_ERROR) DoSomethingWithError(ec);

The while loop is particular to OpenGL, as it can queue up multiple errors.

Non-local error resolution

This kind of error reporting system works well for non-local testing of errors. Once the error code is logged with the owning object, any code that has access to that object can later decide to respond to the error. Thus, the syntactic burden is essentially minimal. CallerFunc needs no special syntax to pass the error to any other code, save ensuring that access to the object that owns the error is provided. In most cases, CallerFunc was given a reference or pointer to the object, so its caller almost always has access to it.

A problem can be created if multiple API functions are called between checking the object's error state, as each individual function call may fail and emit an error. In OpenGL, errors are stored in a FIFO queue; glGetError must be called in a loop as shown above to empty the error queue. By contrast, cairo and iostream only store one error; cairo stores the most recent error, while iostream stores the first error.

When doing non-local testing, there is no syntactic connection between the testing site and the call to TargetFunc that generated the error, save the object that stores the error. It therefore can be difficult to track down the particular CallerFunc that generated the error. This is particularly so if the suite of possible error codes is small, and thus the number of TargetFuncs that could result in any particular code is large.

Communicating such errors across thread boundaries is only as difficult as communicating the object containing those errors across thread boundaries. However, non-local testing creates an additional problem for transmission across threads. Because errors are held in some object, it becomes possible to generate new errors while processing old ones on a different thread.

Different APIs deal with this in different ways.

OpenGL explicitly makes this impossible. In OpenGL, errors are part of the specific OpenGL context that generated them. And OpenGL functions can only be called when a context is "current"; any errors generated go into the current context. Each thread maintains a separate current context, and the context management API explicitly prevents the same context from being current in multiple threads at the same time. Thus, it is impossible to issue errors in one thread while reading them in another.

Cairo and iostream are less forceful. They simply declare that calls into the same object are unsynchronized and can thus cause data races on the error state.

Ignored errors

The syntactic cost of ignoring errors is trivial. It requires nothing more than not checking the error code. This also means that it is virtually impossible to write a tool to detect places where errors are being ignored. The only way to do so is to go through the API's documentation and catalog all functions that can emit an error, then search the code for every use of those functions.

If an ignored error from one function begets multiple errors from later functions, then it becomes difficult to find the specific TargetFunc that caused it. Even if the error system stores the name of the function that emitted an error, one would still need to track down the specific CallerFunc that called it.

In the OpenGL world, this has led to the development of debugging tools, DLLs that slip between the application and OpenGL itself. These are used for function logging, but they can also record which error if any was generated by that function.

Callback alternative

This is a rather uncommon version of out-of-band error reporting. In this scheme, the user can register a callback with the system that will be called whenever an error occurs.

The OpenGL API allows for it, but only in the more recent Debug Output feature. As the name suggests, this feature is only guaranteed to be available when creating debugging OpenGL contexts.

This scheme is mostly useful for logging errors and the like. As a means of actually resolving errors, it is more or less useless, as the callback has a very limited ability to affect control flow. Therefore, this scheme is only noted as a variation, not a full error reporting mechanism on its own.

Exceptions

The exception mechanism is composed of two parts, both built into the C++ language. The first part is within TargetFunc. To signal an error with this mechanism, target function "throws" an arbitrary error code object. Error code objects, when using this mechanism, are called "exceptions".

Throwing an exception operates like a return statement, from a local perspective. Using the throw statement terminates all progress in the current function and returns control to some function higher up in the call stack. Unlike an actual return statement, it does not necessarily return control to its immediate caller. And it certainly does not return control to the location where TargetFunc was called.

To handle an exception requires first bracketing the code that could possibly throw that exception in a try block. This block is followed by one or more catch blocks; each catch block states which specific exception type it handles. Thus, a single try block could catch multiple different exceptions, and resolve them each in a different way.

If code in a try block throws an exception, and one of that try block's catch statements matches the type of the exception that was thrown, then control transfers to that catch block. However, in order to maintain the sanity of the call stack, before control is transfered to the catch block, all automatic objects between the cite of the throw and the receiving catch block must be destroyed, in the reverse order that they were created. This process is called "stack unwinding".

Once the stack has been unwound, control is transferred to the beginning of the catch block, which receives the exception object thrown. This object is the only information that the catch block has about the error besides the exception's type. Once control exits the catch block, the code proceeds as normal (skipping any other catch blocks associated with the try block).

If, for a particular call stack, no appropriate catch block can be found for a particular thrown exception, then std::terminate will be called, halting the application.

Exceptions are the only error mechanism for signaling constructor failure that is recognized by the C++ language itself. Either a constructor succeeds in initializing a valid object, or an exception is thrown. Out-of-band error methods could be used to detect a form of constructor failure. But this usually requires that the object be able to exist in a "created but not valid" state or some otherwise empty state. As far as C++ as a language is concerned, if the constructor did not throw, then the object is valid and it is legal to manipulate it in accord with C++'s rules.

Local error resolution

Locally resolving exceptions requires significant syntax:

try
{
    TargetFunc(...);
}
catch(exception &ec)
{
    //Failure code, perhaps correcting state or returning.
}

//Success or state is otherwise fine.

If the failure condition results in returning to the calling function, then the normal code on success can be folded into the try block:

try
{
    TargetFunc(...);
    //Success
}
catch(exception &ec)
{
    //Failure code, returning control to caller.
}

This puts the error resolution code at the end, removing it from the flow of the main logic.

Exceptions are a separate communication pathway from function return values or parameters, so they interfere with neither.

Non-local error resolution

The syntactic burden for CallerFunc to pass the exception along is, on the surface, nil. Code can look perfectly normal, yet still allow exceptions to pass through it.

However, there are syntactic costs for any code through which exceptions can pass. Local code must structure all resource management as though stack unwinding could happen at any time. If resources are allocated, local code must be designed to ensure that they will be released in the event, not just of that function returning, but also if any function it calls throws an exception.

This need, among others, has given rise to the modern RAII-style of C++ resource management. Resources are bound by objects; resources are acquired in a constructor and are owned by that object. When the object is destroyed, the resource is released. This style of development lets the user ensure that resources will be released during stack unwinding.

Allowing exceptions to pass through resources that are not managed by RAII objects imposes either additional syntactic burden (creating a catch(...) block and freeing resources manually) or faces the risk of leaving resources dangling.

While std::thread itself does not propagate exceptions across threads, std::future and std::promise are capable of doing just that. Indeed, if a promise stores an exception, attempting to get that value from the future will automatically throw that exception. std::async automatically forwards exceptions through the same mechanism.

Ignored errors

There are several levels of safety when it comes to ignoring the possibility of throwing exceptions in CallerFunc. In the lowest level of safety, the code is unsafe for the stack to be unwound through the function. This would be due to the aforementioned lack of RAII-bound resources.

A higher level of safety assumes that any exceptions that pass the function will be handled by someone else. It is safe to stack unwinding, but the code makes no effort to catch exceptions. The latter case requires either the use of RAII objects or using a number of catch(...) blocks to free resources.

Because ignoring exceptions requires no special syntax (in and of itself) it is difficult to statically track down locations where exceptions can pass through. It is still possible, though it would require substantial static analysis tools which have access to a program's full source code.

Uncaught exceptions will result in program termination. And in most debugging systems, such exceptions will point the debugger directly to the source of the exception. Thus, at runtime, it is relatively easy to see where an exception was ignored.

If an exception goes uncaught and terminates the program, this does ensure that the program is terminated due to the uncaught exception. This could be instead of allowing potential data corruption to take place due to continuing to execute the code in an erroneous or invalid state. It could also be instead of allowing the program to proceed ahead, if the exception could have ben safely ignored.

Variant as a return value

This method is conceptually a combination of returning an error code and returning a value that acts as its own error code. The idea is to wrap the desired return type in a type-safe variant, a discriminated union container that knows the type it holds and will stop the user from trying to do the wrong thing with it. This allows functions with return types to return a value or signal an error, without fundamentally changing the return type.

There are two variations of this method, using two different types. The first uses std::optional, part of the library fundamentals v1 DTS. optional is essentially a variant that can either be empty or contain one type of value, supplied by the template parameter. As error code information is not encoded, optional can only say that an operation failed, not why it failed.

The other alternative is expected, which is a variant that cannot be empty, but it can be one of two types, both supplied by template parameters. The first is the expected type, the type that would be returned if the function executed as desired. The second is the error code value, which is returned if the function failed. expected was proposed in paper N4109.

This section will focus on the form of expected defined in the linked proposal, as it is capable of returning actual error code information.

Local error resolution

The syntactic burden for resolving errors locally is very similar to the returned error code method:

auto exp_value = TargetFunc(...);
if(!exp_value)
{
    auto ec = exp_value.error()
    //Failure code, perhaps correcting state or returning.
}

//Success or state is otherwise fine.

However, the user has options to avoid even that. The value_or member function can be used to use a default value if the original is not present:

auto output_value = TargetFunc(...).value_or(default_value);
//Success

This could be considered a form of ignoring the error, but it only applies a default value when an error has occurred. Since different behavior is happening based on the error, it is still effectively handling the error.

Non-local error resolution

Non-local testing can happen in one of two ways. The first way is the traditional method for return-value-based systems: returning the error. This would usually be by extracting and returning the error code itself:

auto exp_value = TargetFunc(...);
if(!exp_value)
{
    auto ec = exp_value.error()
    return ec
}

//Success.
return {}; //Default constructs a non-error.

However, depending on a user's needs, the entire expected object itself could be stored or returned. This would mean that any code that uses it would have to check the value. This is similar in many regards to the special return value method, where it is possible to store an illegal value that outlives CallerFunc. The difference is that excepted forces any code that tries to access it to actually test for the presence of an error.

An alternative way to handle non-local errors is to combine expected with exceptions:

auto output_value = TargetFunc(...).value();
//Success

expected::value will throw an exception if the particular expected object contains an error value. The proposal currently throws its own exception type, wrapping the error value within it. However, if the error type is std::exception_ptr, it calls std::rethrow_exception, so as to do proper exception forwarding.

expected values use value semantics. Therefore, pushing them across threads works to the same extent that any return value can work.

Ignored errors

In most uses of expected, the value type is meaningful to the user. This makes it unlikely for the user to simply discard the return value. So it is impossible to completely ignore the fact that the return is wrapped in an expected. The value_or function makes it possible to minimize the impact, but if the user wants to get the potential value it stores, the user must try to get the value.

This means that it is possible to search for code that uses such constructs. The word "value" is sufficiently common that one would need to use a C++ compiler front-end based tool to do the search without generating too many false positives. It would be less likely for compilers to be willing to issue a warning for using value, as there are legitimate reasons to use it in various locations.

expected can still work for functions which do not return values. Such functions would return the expected<void, error_code> template partial specialization class. However, in terms of ignoring errors, this is not much better than returning the error code itself.

There is one advantage to this construct, however. When it comes to finding places in code where the user has dropped an error, a C++-aware script could search for the use of any function that returns any kind of expected object, regardless of type. If the user discards the return value, then the user is ignoring an error, and therefore is probably doing something wrong. Compilers could even generate warnings based on ignoring expected<void> return values specifically.

optional vs expected

These two types seem similar; they even share the value_or syntax. However, they are quite different semantically. A function which returns optional<T> is saying that it may or may not return a T. But this does not necessarily mean that the lack of a T means that an error happened.

Whereas if function returns an error-valued expected object, this means that some form of error has taken place. The two concepts are syntactically similar, yet semantically different. Thus, it is semantically reasonable to talk about an expected<optional<T>> ec>. That represents a function where returning "null" is not an error, yet the function's processing is capable of erroring out.