Posted on Sat 28 January 2017

Error handling with Status(Or)

I just read an article linked on Hacker News advocating the use of Either<L, R> to signal errors in functions. While I think this is a good start, in my experience Either is too general, the lack of standardizing of the error value makes error handling abstractions hard. How did that file function signal again that a file was not found?

What works a lot better in my experience is a more specialized type Status, with a standardized enum of error codes and a free form error message.

Here's an excerpt from the Status type we use at Google:

enum Code {
  OK = 0,
  CANCELLED = 1,
  UNKNOWN = 2,
  INVALID_ARGUMENT = 3,
  DEADLINE_EXCEEDED = 4,
  NOT_FOUND = 5,
  ALREADY_EXISTS = 6,
  PERMISSION_DENIED = 7,
  UNAUTHENTICATED = 16,
  RESOURCE_EXHAUSTED = 8,
  FAILED_PRECONDITION = 9,
  ABORTED = 10,
  OUT_OF_RANGE = 11,
  UNIMPLEMENTED = 12,
  INTERNAL = 13,
  UNAVAILABLE = 14,
  DATA_LOSS = 15,
};

class Status {
public:
  ...

  bool ok() const;
  int error_code() const;
  StringPiece error_message() const;

  ...
}

This makes working with functions that can fail a lot easier. Want to write a file only if it doesn't exist yet? Just check if the code of the Status returned by file::Open is NOT_FOUND. And since the list of error codes is so small you'll quickly know them all, so you'll know right away how an unknown function will signal possible error cases.

To make debugging easy, you can include extensive free form information in the error message string without making it any more difficult to check the kind of error.

The type above works fine if your function doesn't need to return anything, but what if it does? Not to worry, there is StatusOr<T>, basically Either<Status, T> with some nice accessors.

StatusOr<int> ReadNumber(const std::string& path) {
  std::string data;
  auto status = GetContents(path, &data);
  if (!status.ok()) return status;

  auto number_or = ParseInt(data;)
  if (!number_or.ok()) {
    return number_or.status()
  }

  return number.ConsumeValueOrDie();
}

There's still quite a bit of boiler plate in that code - what's with all the ifs? Luckily, we have some macros to simplify these common checks:

StatusOr<int> ReadNumber(const std::string& path) {
  std::string data;
  RETURN_IF_ERROR(GetContents(path, &data));

  ASSIGN_OR_RETURN(int number, ParseInt(data));
  return number;
}

This is surprisingly useful in code that has to deal with a lot of I/O or computations that can fail, I use Status and StatusOr all the time!

As a more general recommendation, you can find lots of useful code by looking through Google's open source projects. Here are just some examples:

  • logging and assertion macros, e.g. LOG(INFO) << "some status" or CHECK_LT(5, num_shards). These are great for checking invariants and implicit assumptions in your code and make it much easier to track down bugs. There are also DCHECK_* variants that are only executed in debug builds, but I generally always use the normal CHECKs. Every time your code makes some assumptions that would lead to hard to track down problems (memory corruption!) if broken, just add a CHECK.
  • Once, for when you need a (global) value that is initialized exactly once at first use.
  • StringPrintf, like snprintf but much safer: just returns the formatted string as std::string. Also has a StringAppendF variant for when you want to keep appending to one string.
  • StringPiece, a lightweight non-owning view into a std::string or const char* string. Great for when you want to pass around zero-copy references to (sub-)strings, e.g. when parsing. So much simpler than manually passing around offset and length into a shared string.
  • strutil, lots of convenience functions you've needed many times: split a string, join many strings into one, parse a string into various number types, strip whitespace, etc.

And there's a lot more.

Tags: programming

© Julian Schrittwieser. Built using 開板. Theme by Giulio Fidente on github. .