Human Errors

Errors which make your users' lives easier

This crate provides an Error type which has been designed to make errors something which help guide your users through your application rather than blocking their progress. It has fundamentally been designed with the expectation that any failure can be mitigated (even if that means cutting a GitHub issue) and that explaining to your user how to do so is the fastest way to get them moving again.

Features

  • Advice on how to resolve a problem is a fundamental requirement for the creation of an error, making your developers think about the user experience at the point they write the code.
  • Wrapping allows you to expose a causal chain which may incorporate advice from multiple layers in the stack - giving users a better sense of what failed and how to fix it.
  • Integration with the std::error::Error type allows you to wrap any Box-able error in the causal chain and provide additional context.

Getting Started

Rust

# Cargo.toml

[dependencies]
human-errors = "0.1"

Example

use std::fs;
use human_errors::{user_with_internal, Error};

fn main() {
    match read_file() {
        Ok(content) => println!("{}", content),
        Err(err) => eprintln!("{}", err),
    }
}

fn read_file() -> Result<String, Error> {
    fs::read_to_string("example.txt").map_err(|err| user_with_internal(
        "We could not read the contents of the example.txt file.",
        "Check that the file exists and that you have permission to access it.",
        err
    ))?
}

TIP

The above code might result in an error which, when printed, shows the following:

Oh no! We could not read the contents of the example.txt file.

This was caused by:
File Not Found

To try and fix this, you can:
 - Check that the file exists and that you have permission to access it.

Converting Other Errors

When working with errors from other crates and the standard library, you may find it valuable to implement From<OtherError> conversions into human_errors error types.

To make this as easy as possible, we expose a helper macro which will construct a human errors wrapper in your module which can then be easily extended. This macro will publish all of the familiar helper functions you are used to, including:

  • user
  • user_with_cause
  • user_with_internal
  • system
  • system_with_cause
  • system_with_internal

The errors generated by these helper methods will be of the type you provide (MyError in the example below).

error_shim!(MyError);

impl From<std::num::ParseIntError> for MyError {
    fn from(err: std::num::ParseIntError) -> Self {
        user_with_internal(
            "We could not parse the number you provided.",
            "Make sure that you're providing a number in the form 12345 or -12345.",
            err,
        )
    }
}

In some cases, you might find that implementing a generic error conversion does not provide you with adequate context, or is too much effort for a given error. In this case, you can use Rust's built-in map_err method to convert between errors. When doing so, the user_with_internal and system_with_internal helpers can be particularly powerful ways of adding context without losing the underlying error's information.

let input = "https://example.com";

let url: Url = input.parse().map_err(|err| human_errors::user_with_internal(
    &format!("We could not parse '{}' as a URL.", input),
    "Please ensure that your input is a valid URL, including the scheme, like https://google.com/",
    err,
))?;

Background

When rewriting Git-Tool on Rust, one of the big areas I wanted to improve upon was how failures were communicated to the user. In earlier versions, failures were pretty verbosely communicated and it was up to the user to infer, from this, the correct course of action. Sometimes this worked, but in many more cases this left the user confused and unable to make progress.

To address this, I created a custom Error type in Rust which maintained three crucial pieces of information:

  1. Was this failure the result of something the user did, or a failure in the app which they had no control over?
  2. What happened to cause the failure (the plain-english description of what went wrong)?
  3. What can the user do to work around this failure or resolve it?

This corresponded to the following structure in Rust:

pub enum Error {
    UserError(String, String),
    SystemError(String, String),
}

Of course, there are also situations where a higher-order failure occurs because of something further down the stack. Error wrapping is relatively common in different languages, with C# exposing Exception.InnerException and Go taking the approach of appending internal error messages to the outer error.

In my case, I wanted to be able to represent this sequence of failures for the user, to provide them with a sense of how the failure progressed and give them advice which potentially enabled workarounds at each level of the stack.