Error reporting with numerical error codes

The existing Rust error handling/reporting paradigm seems not very appropriate for certain embedded environments. For instance, consider a system that is only able to report numeric error codes via some fixed-width 7-segment display. It would be nice to have core::error::Error-like functionality which works with numerical codes instead of strings as an error-message. I am aware that there is nightly-only Provider API that might help achieving this, but it looks like it is never going to be stable (or not in a foreseeable future).
Of course there are ways to work around the existing limitation by implementing own Error-like traits and types, but I wonder if there are some "standard"-ish approaches in the embedded community to address this.

I’m not familiar with what is existing practice in the embedded space, but from first principles, it seems to me that this should be handled with an application-specific error code enum, possibly combined with implementations of From or a custom trait for existing error types. This is because, to successfully use numeric error codes, it is essential that

  • a single error code isn't used for two unrelated errors, and
  • all possible error codes are known and can be put in the program/product documentation.

You would define an enum:

enum ErrorCode {
    UnknownFault = 0,
    CalibrationFailed = 1,
    LidOpen = 2,
    OutOfPaper = 3,
    OutOfPatience = 4,
}

Then, convert other errors to ErrorCode — or a struct containing it — whenever it is most appropriate in your code; this could be immediately after you receive an error from a library function, or right before you report the error by writing it to the actual LEDs, or whatever else suits. You might also define and implement a trait that adds as_error_code() to other error types.

This is all normal Rust error handling practice, except instead of the final top-level handling being displaying an appropriate user-facing message, it is displaying an appropriate user-facing number. The important part is that by writing the conversions for specific error types, you make sure that the numbers are actually useful to your users. If core::error::Error had a way to return a number, and you used that, you would have no guarantee that the number you obtain matches your intended list of error codes, and you would be at risk of displaying the same number for different errors. Using a custom mechanism gives you a statically-checked guarantee that the codes are codes you intended.

It is harder to do this than to just print an error using the Display implementation that is required by core::error::Error, but this difficulty is part of the problem itself and is not just a missing feature of Error.

7 Likes

Thank you. Yes, a "global" list of possible errors is a separate challenge. It should be easy to add new errors and have them mapped to this global list, which ideally should not require manual maintenance. So far my solutions involved some proc-macro trickery, complicated by the fact that macros are stateless..

I recommend that you just write the enum and not try to make it automatic. Think of it as

  • Keeping things maintainable via simplicity — no magic that could break or confuse things.
  • Investment in your project’s documentation — the enum can be converted into, or automatically checked against, user-facing documentation for what the error codes mean. An automatically compiled list won't be well-documented.
  • Keeping error codes stable — an automatically compiled sequential list would potentially renumber errors, leading to confusion when the version of the program that produced the error is not known confidently.
1 Like

If you define an error type like:

pub struct ErrorCode(NonZeroU8);

then thanks to enum niche optimizations, Result<(), ErrorCode> will be represented in memory using a single byte, with 0 for success, and a non-zero value for error.

This way you can use standard Rust idioms and error handling with ?, but the code compiles to the same bare minimum you'd use when writing low-level code. Rust's call ABI spreads structs over registers, so this is a zero-cost abstraction.

2 Likes