Close

Poor man's exception handling

A project log for SpeakerIR

Enjoying TV with a smart Hi-Fi or speaker? Use your TV IR remote to control volume, instead of having the other one always around.

enrico-gueliEnrico Gueli 02/07/2021 at 11:290 Comments

Once a IR command is received, SpeakerIR will try to send a command to the speaker. When things go wrong, the display will show error messages like "Ev" or "Et". E means error, and the second letter referred to which action failed: "v" is volume change, "t" is TV input select and so on. But that doesn't tell why exactly did it fail :)

Knowing that "the volume-change action failed" doesn't tell me much, because I'm the one issuing the action, so that detail is kinda irrelevant. Sure I'd like to know that my action couldn't be performed, but then a generic "error" would be enough.

That was a limitation I wanted to overcome somehow. For example, I'd like to know if the failure is due to lack of connectivity, or if the speaker didn't respond properly. With such information I can quickly fix the problem, e.g. by turning on the speaker if it was powered off.

The limitation was in the software. When an action is performed, a function is called. That usually returns a boolean value, that is true if everything went fine. If one of the called functions fails, it will return false and a notification will be performed:

void onVolumeUp() {
  if (!increaseVolume(speaker)) {
    notifyVolumeFail();
  }
}

By using simple boolean return values, I can only determine that something went wrong but not why. So I tried to find a better method.

I'd like something similar to exception handling in Java or Kotlin, but in C++ that's quite expensive and not natively supported in Arduino platform. So I rolled my own solution.

First, I created an enumeration type collecting any possible result from my functions. I started with OK then added more values while going through my functions:

/**
 * Definition of result of an operation, either OK or an error value.
 */
enum class Result {
    /** The operation was completed successfully. */
    OK,

    /** We don't know (yet) the speaker address. */
    ERROR_NO_SPEAKER_ADDRESS,

    /** The speaker didn't reply on time, e.g. it's off or its IP address has changed. */
    ERROR_HTTP_TIMEOUT,

    /** The speaker's HTTP server replied with something different than 200 OK. */
    ERROR_HTTP_NON_OK_RESPONSE,

    /** The speaker's HTTP response couldn't be understood. */
    ERROR_PARSE_FAILED
};

Then I replaced every bool return value with Result; and if a function returned an int with a magic value indicating an error, I'd move the return to an output argument (using references) and use Result as return value. I then started using Result values:

Result parseValueInXML(String document, String &output, String openTag, String closeTag) {
  int openIndex = document.indexOf(openTag);
  if (openIndex == -1) {
    return Result::ERROR_PARSE_FAILED;
  }

  int closeIndex = document.indexOf(closeTag, openIndex);
  if (closeIndex == -1) {
    return Result::ERROR_PARSE_FAILED;
  }

  output = document.substring(openIndex + openTag.length(), closeIndex);
  return Result::OK;
}

The next problem was how to propagate errors to the caller. Most of the times I had to call a Result-returning function, and if the result was not OK, stop executing the calling function and return that error result. I found myself writing the same pattern over and over again, so I made a preprocessor macro:

 #define RETURN_IF_ERROR(x) { Result result = (x); if (result != Result::OK) return result; } 

And used it like this:

Result MusicCastSpeaker::getStatus(DynamicJsonDocument &outputDoc)
{
    String url;
    RETURN_IF_ERROR(getZoneUrl(url, "getStatus"))

    request.open("GET", url.c_str());
    request.send();

    RETURN_IF_ERROR(waitForHttpOkResponse(request))
...

The end result is that now the main calling function now "knows" exactly what went wrong, and pass that information to the display for notification:

  Result result = increaseVolume(speaker);
  if (result != Result::OK) {
    notifyFail(result);
  }

The notification function can now consume that information:

void notifyFail(Result result) {
  switch(result) {
  case Result::OK:
    displayText("  OH"); // error: no error? should never happen
    break;
  case Result::ERROR_NO_SPEAKER_ADDRESS:
    displayText("  EA");
    break;
  case Result::ERROR_HTTP_TIMEOUT:
    displayText("  Et");
    break;
  case Result::ERROR_HTTP_NON_OK_RESPONSE:
    displayText("  Eh");
    break;
  case Result::ERROR_PARSE_FAILED:
    displayText("  EP");
    break;

 So here's my poor-man-exception handling framework. It doesn't collect the stack trace, it doesn't collect error-specific information but that's all I need for now.

Discussions