Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

untyped-error

Level: warn · Article: Errors

With exceptions, you catch Error and guess. With typed errors, you match the variant and act. The difference is whether your recovery logic is a strategy or a prayer.

What it checks

A function returns Box<dyn Error>, anyhow::Result/anyhow::Error, eyre, or Result<T, String> / Result<T, &str>. fn main and trait impls are skipped: main may bubble anything, and Error::source is std’s signature, not yours.

Don’t

#![allow(unused)]
fn main() {
fn fetch(url: &Url) -> Result<Response, Box<dyn Error>> {
    Ok(http::get(url)?)
}

fn parse(input: &str) -> Result<Config, String> {
    toml::from_str(input).map_err(|error| error.to_string())
}
}

The caller can display the error. It cannot retry on a timeout, refresh on an expired token, and give up on a validation failure, because it cannot tell them apart.

Do

#![allow(unused)]
fn main() {
enum FetchError {
    Auth { refresh_token: RefreshToken },
    Network { retry_after: Duration },
    RateLimited { retry_after: Duration },
    Validation(ValidationError),
}

fn fetch(url: &Url) -> Result<Response, FetchError> {
    http::get(url).map_err(FetchError::from)
}

impl Client {
    fn fetch_with_retry(&self, url: &Url) -> Result<Response, FetchError> {
        match fetch(url) {
            Ok(response) => Ok(response),
            Err(FetchError::Network { retry_after } | FetchError::RateLimited { retry_after }) => {
                self.clock.sleep(retry_after);
                fetch(url)
            }
            Err(FetchError::Auth { refresh_token }) => {
                self.refresh(refresh_token)?;
                fetch(url)
            }
            Err(error @ FetchError::Validation(_)) => Err(error),
        }
    }
}
}

Granular enough to make different decisions. If two failures get identical handling, they are one variant.

Silence it

#![allow(unused)]
fn main() {
// rabot: allow(untyped-error) CLI entry point: every failure ends in the same exit code and message
fn run(args: Args) -> Result<(), Box<dyn Error>> { .. }
}