Lucky Resistor
Menu
  • Home
  • Learn
    • Learn C++
    • Product Photography for Electronics
      • Required Equipment and Software
    • Soldering for Show
  • Projects
  • Libraries
  • Applications
  • Shop
  • About
    • About Me
    • Contact
    • Stay Informed
  •  
Menu

Consistent Error Handling

Posted on 2019-08-012022-09-04 by Lucky Resistor

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 of bool or int to return a status, because Success and Error make more sense than true, false, 1 or 0.
  • 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 and Why to use Namespaces

How and Why to use Namespaces

Namespaces are a feature of C++ which address the problem of name conflicts. There is a "global" namespace, where everything lives which was declared without namespace. Especially the Arduino environment declares a huge amount of ...
Read More
Units of Measurements for Safe C++ Variables

Units of Measurements for Safe C++ Variables

In your program code, you often deal with unitless values. You can add the units of measurements in the variable or function name, or by adding a comment, but there is still the risk you ...
Read More
Bit Manipulation using Templates

Bit Manipulation using Templates

Did you read my article about C++ templates for embedded code? You learned how to use function templates. This post adds a practical example to this topic. Bit Manipulation You may be familiar with bit ...
Read More
Extreme Integers – Doom from Below

Extreme Integers – Doom from Below

As a beginner or immediate C++ programmer, you heard never mixing unsigned and signed integer types or avoiding unsigned integers at all. There was also this talk about undefined behaviour. Yet, in embedded software development, ...
Read More
Use Enum with More Class!

Use Enum with More Class!

You may be familiar with enum values, but do you know about enum classes? This great feature was introduced with C++11 to solve several problems with the regular enum declaration. Now I will explain the ...
Read More
How to Deal with Badly Written Code

How to Deal with Badly Written Code

Sadly there is a ton of badly written code out in the wild. Hardware related code, seem to suffer more in this regards. I imagine, many developer in this segment are unwillingly to invest time ...
Read More

Leave a Reply Cancel reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Stay Updated

Join me on Mastodon!

Top Posts & Pages

  • How and Why to use Namespaces
  • Storage Boxes System for 3D Print
  • Circle Pattern Generator
  • Use Enum with More Class!
  • Real Time Counter and Integer Overflow
  • Circle Pattern Generator
  • Logic Gates Puzzles
  • C++ Templates for Embedded Code
  • C++ Templates for Embedded Code (Part 2)
  • Logic Gates Puzzle 101

Latest Posts

  • The Importance of Wall Profiles in 3D Printing2023-02-12
  • The Hinges and its Secrets for Perfect PETG Print2023-02-07
  • Better Bridging with Slicer Guides2023-02-04
  • Stronger 3D Printed Parts with Vertical Perimeter Linking2023-02-02
  • Logic Gates Puzzle 1012023-02-02
  • Candlelight Emulation – Complexity with Layering2023-02-01
  • Three Ways to Integrate LED Light Into the Modular Lantern2023-01-29
  • The 3D Printed Modular Lantern2023-01-17

Categories

  • 3D Printing
  • Build
  • Common
  • Fail
  • Fun
  • Learn
  • Projects
  • Puzzle
  • Recommendations
  • Request for Comments
  • Review
  • Software
Copyright (c)2022 by Lucky Resistor. All rights reserved.