Modern error handling in C++

Michał Fita
7 min readJun 7, 2021

To date, the most known method of handling errors in C++ is the exceptions mechanism. However, in many applications (including embedded systems) some features of exceptions are unacceptable. Rust from from start offers a mechanism based on a 2-choice variant, which is widely praised, but criticised by some. In the embedded world it’s a perfect solution. But what about C++?

Exception Handling

The software exception handling first appeared in Lisp 1.5, about 29 years before standardised in C++. They appeared in other languages before that, including Ada — a famous safe language designed for the Department of Defence. What’s interesting Tony Hoare (the author of quicksort and Hoare logic) criticised Ada’s exceptions as dangerous.

In Java, exceptions are available from the beginning and are standard pattern for error handling. Standard libraries are full of hierarchical exceptions of various kinds. What’s interesting here, is that only objects allowed to be thrown have to have java.lang.Throwable in the inheritance hierarchy. This helps to handle exceptions in a universal way using functions with identical signature to log or print out messages related to them. What’s more important, such exceptions carry information about the stack trace (quite often called backtrace these days).

Technically, exceptions in C++ lead to several problems, apart from the code blow, most important are stack unwinding and lack of type restriction — any type can be thrown as an exception. Some claim exceptions create hidden control flow, but that’s a general problem with exception handling in any language. To do exceptions right in C++ a lot of rigour and self-control is required — from my experience most project neglect the problem. In the case of stack unwinding fired by an exception in C++, the serious issues are the unpredictive or non-deterministic time required to complete and the lost backtrace of where the problem occurred. In C++20 std::source_location<> may offer helping hand.

Such problems in a general desktop application or some business software usually isn’t a big deal. In real-time cases (as simple as a device driver in general OS) can be a pain. Steps are usually taken to not use exceptions in projects with real-time needs.

Most traditional pattern

Some form of an old-school dealing with exceptional situations coming from C is returning the error code (usually enum as they’re strongly typed in C++) and passing a reference (a pointer in C) to obtain the result. This model somewhat works, but as there is no clear way to indicate which argument is an input and which argument is output without comment it deteriorates the code readability. One way to express that in the signature is the constness of a reference, but that’s not always clear. Infamous Google Coding Style requires references for read-only data, but pointers for data requiring change — very bad for the code safety.

Another issue we face in this model we need an allocated variable to pass into the function invocation. The C-style pattern often passes a pointer to a pointer and the allocation takes place inside the function. The problem arises when someone mistakenly ignores the returned status and tries to deal with the double-pointer — that’s disastrous.

status_t do_something(result_t** result) { /*...*/ }result_t* result = NULL;
status_t status = do_something(&result);

In C++ we can pass the reference to unique_ptr<>, which’s much safer, but I haven’t seen it being used in such a way too often. I’ve seen the above pattern repeated in C++, sometimes with a reference to the pointer.

status_t do_something(std::unique_ptr<result_t>& result) { /*...*/ }std::unique_ptr<result_t> result;
status_t status = do_something(&result);

The reversion of that pattern would be returning the unique_ptr<> – that’s more often used – and return the status via reference. The advantage of both variants of this pattern is any unique_ptr<> should be checked before use, at least that’s the idiom. Failing to do so doesn’t carry the risk of hard to debug problems.

std::unique_ptr<result_t> do_something(status_t& result) { /*...*/ }status_t status = status_t::io_error;
std::unique_ptr<result_t> result = do_something(&status);

Error handling in Rust

From the perspective of the function invocation, it’s always clear for the code reader when the result is returned. In Rust, the Result<Value, Error> type allows to return both the value if things went well or the error status when went rogue.

fn do_something() -> Result<u64, Status> { /* ... */ }let result = do_something();
if let Ok(r) = result { /* deal with r:u64 */ }

With the new ? operator early returns in error cases are handled abstractly behind our backs leading to cleaner code. That’s how our code may look like if we don’t use ? operator:

fn do_something() -> Result<u64, Status> {
let result = do_something_prior();
if let Err(e) = result { return e };
let value = result.unwrap();
/* deal with the value and return Ok(...) */
}

But the operator make it much cleaner:

fn do_something() -> Result<u64, Status) {
let value = do_something_prior()?;
/* deal with the value... */
}

The Rust 2018 Edition Guide has even a better fuller example.

What I find astonishing is that the Error is not constrained. I’d expect a minimalist trait that would characterise any error type. It seems the authors of Rust didn’t want to constrain what’s error, especially for no_std code2. The Rust Standard Library has a proper trait std::error::Error for the job, so more a constrained Result type would be declared:

#[must_use = "this `Result` may be an `Err` variant, which should be handled"]
pub enum Result<T, E: std::error::Error> {
Ok(T),
Err(E),
}

Some C++ alternatives

Many projects try to either deal with this using std::optional<>, std::tuple<> or std::variant<> or a combination of any of them, some write an in-house version of Rust’s solution. All nice, but consistency sucks and it’s hardly exportable in a library.

Modern solution

Herb Sutter in P0709r4 proposes zero overhead exceptions by restricting throw to accept only values. But his paper discusses various approaches, including the one below. It duly notes the fact in C++ multiple approaches are already used, and standardisation of more of them may not exactly help. It’s worth at least reading briefly.

The P0323r10 proposal offers a solution that looks like copied from Rust. std::expect<T, E> is a type to return a result object, and it can be checked similar way as optional, but instead of being empty can carry an object for error (called unexpected in the proposal). If you connect std::expect<> with [[nodiscard]] attribute you wield a pretty consistent mechanism for handling errors in the code1.

Keep in mind, however, E can be any type – one of the consistency problems exceptions have at the moment is not solved. I’d personally prefer if that type is constrained by at least concept, that would make logging unexpected situations much cleaner with a common function. It’s up to the programmer to enable various error types to cast to each other.

outcome<T> was proposed as the specialisation of expected<T, E> but despite implementation in Boost, didn’t gain enough support in the standardisation committee to be considered into the standard. The more generic type from P0323 is expected to appear in C++23.

Another rather fresh proposal from P2232r0 suggest restriction of throwable types to static ones. This is fresh, so I’m not going to comment on this.

Removal of exceptions

One of the things most embedded programmers do in their C++ project is to disable exceptions. There are three reasons, some refer to points already mentioned above:

  1. Reduction of code size — exception handling mechanisms can use a significant portion of code memory on small resource systems
  2. Determinism — exception handling is non-deterministic for a programmer in a real time code
  3. C ABI boundary — C++ exception handling don’t play well with C code widely used in the implementation of RTOSes, third party libraries and device drivers

However, the standard library is not yet ready for such a paradigm shift and effectively any attempt to throw an exception there is converted into a halt — on embedded systems it usually means a reboot. So, protection against rouge values, like on the programmers’ side is making the writing process pretty tedious. Unit tests help cover bad scenarios pretty well, however, the whole thing is far from ideal. Many people in the embedded arena expect the language without exceptions is going to be standardised including a variant of standard library… eventually, someone is going to propose an alternative.

Lightweight exceptions could be one (resource-wise), but whether they can gain enough traction remains unanswered. Maybe if C language would introduce them helping in keeping the compatible ABI, that would shift things a bit.

To date, Rust demonstrated its Result<T, E> works and it’s widely accepted. When FFI would support mapping Rust’s type with C++ things would be practically sorted forever, as many more are going to look to add Rust to the existing C++ codebases.

From Experience

I had the pleasure to work with a mechanism very similar to the proposed one, that came into our project as a result of the efforts of my colleague to wipe the use of exceptions. Removal of exceptions and disabling them in code saved us a lot of code space in the 2MB we had available. That in-house solution was, however, a little bit more closer to outcome<T> as there was one Error type.

What’s important is the experience for the programmer. Dealing with the code where you have to deal with the result prevents undeliberate omission of handling of the error returned:

auto r = screwdriver();
if (!r.is_ok()) {
// deal with the r.error()
return r.error();
}

The above example is a simplified illustration of what you have to deal with. Errors can easily be passed down — in case of expect<> they would have to be compatible; in our case, they were as such out of the box.

If I compare that with a similar experience from Rust’s standard, this is a really good solution — I’m looking forward to wider application. C++ doesn’t have ? operator to simplify all returns in case of error, meaning more boilerplate is required, but maybe one day even that could be improved.

Existing implementations

I know two implementations of expected<> that follows the specification from the proposal worth mentioning:

If they’re others, I haven’t checked them and they’re probably not very popular.

References

Disclaimer: This articles express only the view of the author and cannot be linked with any entity to which the author may have ties, neither personal nor professional.

1 — I only wish the standard library would have an exceptions-free variant using this mechanism instead.

2 — The no_std is a crate build without Rust standard library, for example for a bare-metal embedded system; an equivalent of -nostdlib in GCC.

--

--

Michał Fita

Software Craftsman who started programming before even had a computer.