Error handling in firmware is a difficult task. If you decide to ignore errors, the best you can expect is a deadlock, but you could also damage the hardware.
When reviewing existing code, I often find boolean return values or a system of error codes. The problem with these systems is the lack of readability. It is hard to say whether true is the result of a successful call or means an error.
Here I will explain different methods to handle errors in your firmware. Although the examples I provide are for the Arduino Uno, the concepts will work with any platform and with all modern C++ compilers.
Initial Example
Let’s have a look at this next example:
const uint8_t cInputPin = 12; const uint8_t cErrorLed = 13; const uint32_t cSignalTimeoutMs = 2000; bool waitForSignal() { const auto timeout = millis() + cSignalTimeoutMs; do { if (digitalRead(cInputPin) == LOW) { return true; } } while (static_cast<int32_t>(timeout - millis()) >= 0); return false; } void error() { Serial.println("error"); Serial.flush(); while (true) { digitalWrite(cErrorLed, HIGH); delay(100); digitalWrite(cErrorLed, LOW); delay(100); } } void setup() { Serial.begin(115200); while (!Serial) {} pinMode(cInputPin, INPUT_PULLUP); if (!waitForSignal()) { error(); } } void loop() { // ... }
It is possible to improve the example code with unit types and proper handling of the real-time counter. Also, the proper API documentation is missing. However, I will ignore these concepts in this article to keep the code simple and the dependencies low.
The main issue here is the readability of the error handling code. Examine the following line out of context:
if (!waitForSignal()) {
You read this line as: “if not wait for signal”. This makes no sense and you need the whole context to understand what you are actually testing here.
I am aware that you see boolean values everywhere for error handling. It is indeed a widespread practice to use them as return values for this task. Nevertheless, there are better alternatives, which I will explain shortly.
Using an Enum Class
I found it useful to work with enum classes for error handling.
enum class Status : uint8_t { Success, Error, };
In contrast to a bool
or integer type, the meaning of this enum value is always clear.
const uint8_t cInputPin = 12; const uint8_t cErrorLed = 13; const uint32_t cSignalTimeoutMs = 2000; enum class Status : uint8_t { Success, Error }; Status waitForSignal() { const auto timeout = millis() + cSignalTimeoutMs; do { if (digitalRead(cInputPin) == LOW) { return Status::Success; } } while (static_cast<int32_t>(timeout - millis()) >= 0); return Status::Error; } void error() { Serial.println("error"); Serial.flush(); while (true) { digitalWrite(cErrorLed, HIGH); delay(100); digitalWrite(cErrorLed, LOW); delay(100); } } void setup() { Serial.begin(115200); while (!Serial) {} pinMode(cInputPin, INPUT_PULLUP); if (waitForSignal() == Status::Error) { error(); } } void loop() { // ... }
You can compile this code and compare it to the previous example. The firmware binaries you get are byte equal because the C++ compiler handles boolean values in most cases as single-byte values.
Concise Errors
Let us now write a simple driver for a peripheral. This peripheral can be optionally attached to our hardware, and its presence is detected by pulling one pin low.
#pragma once #include <Arduino.h> namespace Grinder { enum class Status : uint8_t { Success, NotConnected, Timeout, }; Status initialize(); }
#include "Grinder.hpp" namespace Grinder { const uint8_t cIsConnectedPin = 11; const uint8_t cInputPin = 12; const uint32_t cSignalTimeoutMs = 2000; Status waitForSignal() { const auto timeout = millis() + cSignalTimeoutMs; do { if (digitalRead(cInputPin) == LOW) { return Status::Success; } } while (static_cast<int32_t>(timeout - millis()) >= 0); return Status::Timeout; } Status initialize() { pinMode(cIsConnectedPin, INPUT_PULLUP); pinMode(cInputPin, INPUT_PULLUP); if (digitalRead(cIsConnectedPin) == HIGH) { return Status::NotConnected; } Status status; if ((status = waitForSignal()) != Status::Success) { return status; } return Status::Success; } }
#include "Grinder.hpp" const uint8_t cErrorLed = 13; bool gGrinderConnected = false; void error() { Serial.println("error"); Serial.flush(); while (true) { digitalWrite(cErrorLed, HIGH); delay(100); digitalWrite(cErrorLed, LOW); delay(100); } } void setup() { Serial.begin(115200); while (!Serial) {} const auto status = Grinder::initialize(); if (status == Grinder::Status::Timeout) { error(); } else if (status == Grinder::Status::Success) { gGrinderConnected = true; } } void loop() { // ... }
I create the Status
enum in the Grinder
namespace, because each module can have its own set of status messages. It is not the generic Status
enum, but rather a specialized Grinder::Status
for this module.
With five modules, you get five different Status
enums, one in each namespace. Due to the long names, such as Grinder::Status::Success
, the sourcecode quickly loses readability.
Simple Status Checks
Let us simplify common status checks using function templates.
#pragma once template<typename StatusEnum> constexpr bool isSuccessful(StatusEnum status) { return status == StatusEnum::Success; } template<typename StatusEnum> constexpr bool hasError(StatusEnum status) { return status != StatusEnum::Success; } template<typename StatusEnum> constexpr bool hasTimeout(StatusEnum status) { return status != StatusEnum::Timeout; }
These neat function templates will work with any status enum that features a ::Success
or a ::Timeout
entry.
You can read my article about function templates to better understand the declarations in this example code.
Before we apply these new tools to our main file, we need to update Grinder.cpp
first:
#include "Grinder.hpp" #include "StatusTools.hpp" namespace Grinder { const uint8_t cIsConnectedPin = 11; const uint8_t cInputPin = 12; const uint32_t cSignalTimeoutMs = 2000; Status waitForSignal() { const auto timeout = millis() + cSignalTimeoutMs; do { if (digitalRead(cInputPin) == LOW) { return Status::Success; } } while (static_cast<int32_t>(timeout - millis()) >= 0); return Status::Timeout; } Status initialize() { pinMode(cIsConnectedPin, INPUT_PULLUP); pinMode(cInputPin, INPUT_PULLUP); if (digitalRead(cIsConnectedPin) == HIGH) { return Status::NotConnected; } if (hasError(waitForSignal())) { return Status::Timeout; } return Status::Success; } }
This new syntax is short, crisp, and clear. There is now no doubt as to which situation you test in the if
statement.
Now we’ll move on to the main file and apply the function templates there.
#include "Grinder.hpp" #include "StatusTools.hpp" const uint8_t cErrorLed = 13; bool gGrinderConnected = false; void error() { Serial.println("error"); Serial.flush(); while (true) { digitalWrite(cErrorLed, HIGH); delay(100); digitalWrite(cErrorLed, LOW); delay(100); } } void setup() { Serial.begin(115200); while (!Serial) {} const auto status = Grinder::initialize(); if (hasTimeout(status)) { error(); } else if (isSuccessful(status)) { gGrinderConnected = true; } } void loop() { // ... }
We successfully solved the problems related to the namespaces and improved the code’s readability.
Sharing a Simple Status Enum
In most cases, you just need a simple status with a Success
and Error
value. In these cases, it would be repetitive to declare this enumeration again and again in your code. Instead, it is better to share this simple status where it is required.
To illustrate this technique, I created the following abstract example:
#include "Display.hpp" #include "InputPanel.hpp" #include "Crane.hpp" void setup() { if (hasError(Display::initialize())) { Display::error(); } if (hasError(InputPanel::initialize())) { Display::error(); } if (hasError(Crane::initialize())) { Display::error(); } } void loop() { const auto button = InputPanel::getButtonPress(); using Button = InputPanel::Button; if (button == Button::Up) { if (hasError(Crane::moveUp())) { Display::error(); } } else if (button == Button::Down) { if (hasError(Crane::moveDown())) { Display::error(); } } delay(100); }
#pragma once #include "StatusTools.hpp" namespace Crane { /// The status for all calls. /// using Status = SimpleStatus; /// Initialize the crane module. /// Status initialize(); /// Move the crane up /// Status moveUp(); /// Move the crane down /// Status moveDown(); }
#pragma once #include "StatusTools.hpp" namespace Display { /// The status for all calls. /// using Status = SimpleStatus; /// Initialize the display. /// Status initialize(); /// Display a fatal error. /// void error(); /// Show the given text on the display. /// Status showText(const char *text); }
#pragma once #include "StatusTools.hpp" namespace InputPanel { /// The status for all calls. /// using Status = SimpleStatus; /// The buttons. /// enum class Button : uint8_t { None = 0, Up = (1u << 0), Down = (1u << 1), Left = (1u << 2), Right = (1u << 3), Select = (1u << 4), }; /// Initialize the input panel. /// Status initialize(); /// Get the next button press. /// Button getButtonPress(); }
#pragma once #include <Arduino.h> /// A simple status. /// enum class SimpleStatus : uint8_t { Success, Error }; /// Check if a function was executed successfully. /// /// @return `true` if the function was executed successfully. /// template<typename StatusEnum> constexpr bool isSuccessful(StatusEnum status) { return status == StatusEnum::Success; } /// Check if a function failed with any error. /// /// @return `true` if the function returned anything else than `Success`. /// template<typename StatusEnum> constexpr bool hasError(StatusEnum status) { return status != StatusEnum::Success; } /// Check if a function returned with a timeout. /// /// @return `true` if the function returned the status `Timeout`. /// template<typename StatusEnum> constexpr bool hasTimeout(StatusEnum status) { return status != StatusEnum::Timeout; }
I first added a new SimpleStatus
enumeration in the StatusTools.hpp
file. From there, I use it in all my interfaces with a using
statement:
using Status = SimpleStatus;
You may wonder why I do not use SimpleStatus
directly; I do this to keep my code extensible.
If I extend one of these modules and require additional status values, I can easily replace the using
directive with an enumeration. Ideally, there is no change required in the rest of the code.
Results with Values
Sometimes a function must return a status and one or more results. You may have experienced interfaces such as this:
bool getRgb(float &r, float &g, float &b); int getDistance(bool *ok);
For both interfaces, the status and the value are not returned using the same mechanism. One part of the result is passed to the user as return value and the other one using parameters.
While there is nothing wrong with this kind of interfaces, they can easily confuse the user. This contradicts the primary goal of designing interfaces. You must design interfaces so they can be used in a clean and simple way.
float red; float green; float blue; if (!getRgb(&red, &green, &blue)) { error(); } Color color(red, green, blue); // ...
bool ok; const auto distance = getDistance(&ok); if (!ok) { error(); } // ...
Introducing a Result Class
Let’s now write a new class for the StatusTools.hpp
file to return the status and value as a bundle.
// in StatusTools.hpp template<typename StatusEnum, typename ValueType> class StatusResult { public: constexpr static StatusResult success(ValueType value) { return StatusResult(StatusEnum::Success, value); } constexpr static StatusResult error(StatusEnum status = StatusEnum::Error) { return StatusResult(status); } constexpr bool isSuccess() const { return _status == StatusEnum::Success; } constexpr bool hasError() const { return _status != StatusEnum::Success; } constexpr ValueType getValue() const { return _value; } private: constexpr StatusResult(StatusEnum status, ValueType value = {}) : _status(status), _value(value) { } private: StatusEnum _status; ValueType _value; };
This new template class has no public constructor, but two static
functions instead that are used to create new instances. I do this to enforce readable code and prevent any misuse of the class.
We extend the Grinder
interface with a new method getCurrentSpeed()
:
Grinder.hpp
#pragma once #include "StatusTools.hpp" #include <Arduino.h> namespace Grinder { enum class Status : uint8_t { Success, NotConnected, Timeout, }; using IntResult = StatusResult<Status, uint16_t>; Status initialize(); IntResult getCurrentSpeed(); }
To keep our code clean and simple, we declare a new name, IntResult
, for the StatusResult
type. It uses the Status
enum we declared for our interface and a uint16_t
value as return type.
An implementation of the new function looks similar to this:
Grinder.cpp
#include "Grinder.hpp" namespace Grinder { // ... IntResult getCurrentSpeed() { Status status; if (hasError(status = waitForSignal())) { return IntResult::error(status); } const auto speed = static_cast<uint16_t>(analogRead(cSpeedPin)); return IntResult::success(speed); } }
The implementation visualizes the reason the result class uses the two class methods error
and success
to create instances. If we had used a constructor directly, the code would have looked like this:
IntResult getCurrentSpeed() { Status status; if (hasError(status = waitForSignal())) { return IntResult(status); } const auto speed = static_cast<uint16_t>(analogRead(cSpeedPin)); return IntResult(speed); }
This code is shorter, but is more difficult to read and understand.
Using the Interface
Let’s take a look at how we use this new interface:
void loop() { auto result = Grinder::getCurrentSpeed(); if (result.isSuccessful()) { Serial.print("Current speed = "); Serial.println(result.getValue()); } else { Serial.println("Failed to get current speed."); } delay(1000); }
The code to use our interface is simple and easy to understand. After retrieving the result, you can test it to verify success and use its value.
Allow a Consistent Usage
We can make our code more consistent with only a small addition to the result class:
template<typename StatusEnum, typename ValueType> class StatusResult { public: // ... constexpr bool operator==(StatusEnum other) const { return _status == other; } constexpr bool operator!=(StatusEnum other) const { return !operator==(other); } // ... };
Here, we add comparison operators to the class. These operators allow for a direct comparison with a status:
void loop() { auto result = Grinder::getCurrentSpeed(); if (result == Grinder::Status::Success) { Serial.print("Current speed = "); Serial.println(result.getValue()); } else { Serial.println("Failed to get current speed."); } delay(1000); }
This is beneficial for very specific status values, but we wrote the isSuccessful
and hasError
functions to make the code readable.
So, let’s address this problem as well. We add two function templates to StatusTools.hpp
to handle every StatusResult
:
template<typename StatusEnum, typename ValueType> constexpr bool isSuccessful(const StatusResult<StatusEnum, ValueType>& result) { return result.isSuccessful(); } template<typename StatusEnum, typename ValueType> constexpr bool hasError(const StatusResult<StatusEnum, ValueType>& result) { return result.hasError(); }
Now we can use isSuccessful
for StatusResult
as well:
void loop() { auto result = Grinder::getCurrentSpeed(); if (isSuccessful(result)) { Serial.print("Current speed = "); Serial.println(result.getValue()); } else { Serial.println("Failed to get current speed."); } delay(1000); }
Recap
- Use
enum
instead ofbool
orint
to return a status, becauseSuccess
andError
make more sense thantrue
,false
,1
or0
. - Declare an independent
Status
enumeration in each interface. - Keep your code readable using function templates like
isSuccessful
. - For the most common cases, declare a
SimpleStatus
enumeration that you can use in all your interfaces. - Do not separate the status from the returned values. Instead, create a
StatusResult
class for this use case.
Conclusion
Handling errors in a proper manner remains a difficult task. But with the right techniques, you can implement them in a clear and consistent way that will undoubtedly improve your code. Your primary goal must be to achieve the highest code quality and readability, and only these methods allow you to spot problems in the firmware early in the process.
If you have any questions, missed any information, or simply wish to provide feedback, simply add a comment below!
Learn More

How to Deal with Badly Written Code

How and Why to use Namespaces

Units of Measurements for Safe C++ Variables

Guide to Modular Firmware

Use Enum with More Class!
