CodeSampleX

Exemple

anyhow 1.0.104: Handle errors in a Rust application with anyhow: attach context, keep the cause chain, log it without losing the cause, and recover the concrete error

Échantillon vérifié pour cargo anyhow 1.0.104: Handle errors in a Rust application with anyhow: attach context, keep the cause chain, log it without losing…

sha256:709971847660bb4f230ee832aa66da4263481bd8d5e0a498178ee3b789c53404

Ce réseau offre une seule chose : un échantillon qui compile. Il l'a exécuté dans un bac à sable et conservé le reçu signé. Il ne note rien et ne garantit rien : si le même code compile chez vous, il ne l'a pas mesuré. Combien de clés de signature distinctes ont déposé un reçu de contrat réussi. Une seule, c'est l'auteur ; plus d'une signifie que quelqu'un d'autre l'a compilé aussi. Une clé est auto-générée sans identité enregistrée derrière, donc on compte des clés, pas des personnes. MIT-0

Preuves d'exécution

L'environnement déclaré et les exécutions signées sont séparés, pour que vous voyiez exactement ce que cet échantillon a exécuté et où.

Base de preuve
Contrat signé réussi
Reçus de vérification
3
Clés de signature qui l’ont compilé
3
Environnement déclaré rust 1 linux x64 rust 1 rust cargo

Environnements des exécutions de vérification

Environnement Contrat Étapes Exécution
rust 1 · linux alpine/x64 · docker ed25519:a2ec939a4c60e243 PASS compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS
CONTAINER_RUN · cargo@1
2026-08-14
rust 1 · linux alpine/x64 · docker ed25519:2175b912ea1c23b1 PASS compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS
CONTAINER_RUN · cargo@1
2026-08-18
rust 1 · linux alpine/x64 · docker ed25519:c1973797be207ac4 PASS compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS
CONTAINER_RUN · cargo@1rust:1-alpine@sha256:a10e64dd139b…
2026-09-08

Cas

HOW
Objectif
Handle errors in a Rust application with anyhow: attach context, keep the cause chain, log it without losing the cause, and recover the concrete error
Paquets
Symboles
  • anyhow::Context::context
  • anyhow::Context::with_context
  • anyhow::Error::chain
  • anyhow::Error::root_cause
  • anyhow::Error::downcast_ref
  • anyhow::bail
  • anyhow::ensure
Environnement
rust 1
Créé
2026-08-14T08:53:52Z

Contrat

  1. assert the bare io::Error for a missing path is NotFound and its Display carries os error 2
  2. assert Display of a context-wrapped error prints only the outermost context, not the io message
  3. assert alternate Display {:#} flattens the whole chain into one colon-joined line while {} does not
  4. assert chain() yields both context frames then the original io error, in that order
  5. assert root_cause() returns the original io error at the bottom of the chain
  6. assert Debug {:?} prints the multi-line report with a Caused by section
  7. assert anyhow::Error derefs to a dyn StdError that prints as the outermost frame, and that source() through that Deref yields the next frame
  8. assert downcast_ref recovers the io::Error and its NotFound kind through two layers of context
  9. assert downcast_ref recovers a concrete thiserror library type out of a context-wrapped anyhow error
  10. assert anyhow::Error is one pointer wide, smaller than the concrete thiserror error, which is as wide as its String field
  11. assert bail! and ensure! build a one-frame error from a plain message with no source
  12. assert an interpolated bail! message downcasts to String while an anyhow! bare literal downcasts to &str
  13. assert io::Error and ParseIntError both convert through ? with no From impl written
  14. assert context() on a None Option produces an error with no source
  15. assert with_context never runs its closure on the success path while context() builds its message even when the Result is Ok

Fichiers

  • Cargo.lock
  • Cargo.toml
  • csx.json
  • src/main.rs

Télécharger l’artefact source (tar.gz)

Code source

Cargo.lock
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4

[[package]]
name = "anyhow"
version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"

[[package]]
name = "csx-sample-anyhow"
version = "1.0.0"
dependencies = [
 "anyhow",
 "thiserror",
]

[[package]]
name = "proc-macro2"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
dependencies = [
 "unicode-ident",
]

[[package]]
name = "quote"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [
 "proc-macro2",
]

[[package]]
name = "syn"
version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
dependencies = [
 "proc-macro2",
 "quote",
 "unicode-ident",
]

[[package]]
name = "thiserror"
version = "2.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f"
dependencies = [
 "thiserror-impl",
]

[[package]]
name = "thiserror-impl"
version = "2.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
dependencies = [
 "proc-macro2",
 "quote",
 "syn",
]

[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
Cargo.toml
[package]
name = "csx-sample-anyhow"
version = "1.0.0"
edition = "2021"
license = "MIT-0"

[dependencies]
anyhow = "1.0.104"
thiserror = "2.0.20"
csx.json
{"case":{"caseId":"case:sha256:693f47ea50a1edfd061457ba683289306ba9ebd242c8ad0f57880ccb2c1cb8be","constraints":{"runtime":"rust"},"contract":["assert the bare io::Error for a missing path is NotFound and its Display carries os error 2","assert Display of a context-wrapped error prints only the outermost context, not the io message","assert alternate Display {:#} flattens the whole chain into one colon-joined line while {} does not","assert chain() yields both context frames then the original io error, in that order","assert root_cause() returns the original io error at the bottom of the chain","assert Debug {:?} prints the multi-line report with a Caused by section","assert anyhow::Error derefs to a dyn StdError that prints as the outermost frame, and that source() through that Deref yields the next frame","assert downcast_ref recovers the io::Error and its NotFound kind through two layers of context","assert downcast_ref recovers a concrete thiserror library type out of a context-wrapped anyhow error","assert anyhow::Error is one pointer wide, smaller than the concrete thiserror error, which is as wide as its String field","assert bail! and ensure! build a one-frame error from a plain message with no source","assert an interpolated bail! message downcasts to String while an anyhow! bare literal downcasts to \u0026str","assert io::Error and ParseIntError both convert through ? with no From impl written","assert context() on a None Option produces an error with no source","assert with_context never runs its closure on the success path while context() builds its message even when the Result is Ok"],"goal":"Handle errors in a Rust application with anyhow: attach context, keep the cause chain, log it without losing the cause, and recover the concrete error","kind":"HOW","packages":["pkg:cargo/anyhow@1.0.104","pkg:cargo/thiserror@2.0.20"],"schemaVersion":1,"symbols":["anyhow::Context::context","anyhow::Context::with_context","anyhow::Error::chain","anyhow::Error::root_cause","anyhow::Error::downcast_ref","anyhow::bail","anyhow::ensure"]},"contractCommand":["cargo","run","--offline","--quiet"],"environment":{"arch":"x64","ecosystem":"cargo","executionContext":"rust","language":"rust","os":"linux","packageManager":"cargo","runtime":"rust","runtimeVersion":"1","schemaVersion":1},"license":"MIT-0","packages":["pkg:cargo/anyhow@1.0.104","pkg:cargo/thiserror@2.0.20"],"schemaVersion":1,"symbols":["anyhow::Context::context","anyhow::Context::with_context","anyhow::Error::chain","anyhow::Error::root_cause","anyhow::Error::downcast_ref","anyhow::bail","anyhow::ensure"],"verifierAdapter":"cargo@1"}
src/main.rs
use std::cell::Cell;
use std::error::Error as StdError;
use std::fs;
use std::io;
use std::mem::size_of;

use anyhow::{anyhow, bail, ensure, Context, Result};
use thiserror::Error;

/// anyhow is the application half of the pair. thiserror gives a library one
/// named type per failure mode so callers can match on it; anyhow gives a
/// binary ONE opaque type that swallows anything convertible into a std error
/// and carries a chain of human context on the way up. Nothing here writes a
/// single From impl: anyhow has a blanket
/// `impl<E: StdError + Send + Sync + 'static> From<E> for Error`. That impl is
/// also why anyhow::Error is not itself a std::error::Error — a type carrying
/// that blanket From cannot also implement StdError without colliding with
/// core's reflexive `impl<T> From<T> for T` (rustc rejects the pair with
/// E0119), so handing `&err` to something that wants `&dyn StdError` is E0277.
/// Those two are compile-time facts, checked by compiling them, and nothing
/// below asserts them. What the contract does exercise is the consequence:
/// Error derefs to `dyn StdError + Send + Sync`, and that Deref is the only
/// reason `source()` resolves on it at all.
///
/// The trap this sample exists for is the formatting. `{}` on an anyhow error
/// prints ONLY the outermost context, so `log::error!("{e}")` throws away the
/// io error that actually explains the failure. `{:#}` flattens the whole
/// chain onto one line, `{:?}` prints the multi-line report with "Caused by:".
/// Pick one deliberately; the default is the lossy one.

/// The thiserror side of the contrast: a concrete, matchable library error.
/// It survives inside anyhow::Error and comes back out with downcast_ref.
#[derive(Debug, Error)]
#[error("sample {id} is not cached")]
struct NotCached {
    id: String,
}

const MISSING: &str = "/nonexistent/csx/manifest.json";

fn read_manifest(path: &str) -> Result<String> {
    // `?` on io::Error with no From impl in sight, plus lazily formatted
    // context. with_context defers the format! to the error path; context()
    // would build the string on every successful call too.
    let text = fs::read_to_string(path).with_context(|| format!("reading {path}"))?;
    Ok(text)
}

fn load_sample(path: &str) -> Result<String> {
    // Context applied to a Result whose error is already an anyhow::Error:
    // this pushes a second layer instead of replacing the first.
    read_manifest(path).context("loading the cached sample")
}

fn lookup(id: &str) -> Result<&'static str> {
    if id != "sha256:abc" {
        return Err(NotCached { id: id.to_string() }).context("resolving the sample id");
    }
    Ok("cached")
}

fn parse_size(text: &str) -> Result<u64> {
    // ensure! and bail! build an error out of a plain message, with no error
    // type declared anywhere. That is the whole point of anyhow in a binary:
    // a failure that nobody will ever match on does not need a type.
    ensure!(!text.is_empty(), "manifest is empty");
    if text == "unknown" {
        bail!("size {text:?} is not a number");
    }
    Ok(text.parse()?)
}

fn main() {
    // The strerror text comes from the platform libc, so the exact wording is
    // read from a live error rather than hardcoded; the structure around it is
    // what this sample asserts.
    let direct = fs::read_to_string(MISSING).unwrap_err();
    let os_message = direct.to_string();
    assert_eq!(direct.kind(), io::ErrorKind::NotFound);
    assert!(os_message.contains("os error 2"), "{os_message}");

    let err = load_sample(MISSING).unwrap_err();

    // Display shows the outermost context and NOTHING else. The cause is
    // still in there, it is just not printed — this is the logging bug.
    assert_eq!(err.to_string(), "loading the cached sample");
    assert!(!err.to_string().contains("os error"));

    // Alternate Display flattens the whole chain into one line, joined with
    // ": ". Same error value, same {} vs {:#} choice, completely different
    // amount of information.
    assert_eq!(
        format!("{err:#}"),
        format!("loading the cached sample: reading {MISSING}: {os_message}"),
    );
    assert_eq!(format!("{err}"), "loading the cached sample");

    // chain() walks outward-in: every context frame in the order it was
    // attached, most recent first, then the original error last.
    let chain: Vec<String> = err.chain().map(|e| e.to_string()).collect();
    assert_eq!(
        chain,
        vec![
            "loading the cached sample".to_string(),
            format!("reading {MISSING}"),
            os_message.clone(),
        ],
    );
    assert_eq!(err.root_cause().to_string(), os_message);

    // Debug is the report format, not the derived struct dump.
    let report = format!("{err:?}");
    assert!(report.starts_with("loading the cached sample"), "{report}");
    assert!(report.contains("Caused by:"), "{report}");
    assert!(report.contains(&os_message), "{report}");
    assert!(report.lines().count() > 1);

    // anyhow::Error is not a std error, but it derefs to one: the target is the
    // outermost frame itself, printing exactly what `{}` printed. Error has no
    // inherent source(), so the call below is method resolution going through
    // this Deref to reach StdError::source.
    let as_std: &(dyn StdError + Send + Sync + 'static) = &*err;
    assert_eq!(as_std.to_string(), "loading the cached sample");
    let next: &(dyn StdError + 'static) = err.source().expect("a source is attached");
    assert_eq!(next.to_string(), format!("reading {MISSING}"));

    // Two layers of context did not consume the concrete error: downcast_ref
    // reaches through the chain and hands back the real io::Error, kind and
    // all. Opaque to formatting, still typed when you need it.
    assert!(err.is::<io::Error>());
    let recovered = err.downcast_ref::<io::Error>().expect("io::Error is in the chain");
    assert_eq!(recovered.kind(), io::ErrorKind::NotFound);
    assert!(err.downcast_ref::<NotCached>().is_none());

    // The library error type comes back out the same way, which is how an
    // application handles one specific failure without giving up anyhow.
    let missing = lookup("sha256:zzz").unwrap_err();
    assert_eq!(missing.to_string(), "resolving the sample id");
    assert_eq!(
        missing.downcast_ref::<NotCached>().map(|e| e.id.as_str()),
        Some("sha256:zzz"),
    );
    assert_eq!(format!("{missing:#}"), "resolving the sample id: sample sha256:zzz is not cached");
    assert_eq!(lookup("sha256:abc").unwrap(), "cached");

    // One word wide no matter what it carries, because everything is boxed —
    // the thiserror struct is as big as its fields. This is why anyhow::Result
    // stays cheap to return and why the type tells a caller nothing.
    assert_eq!(size_of::<anyhow::Error>(), size_of::<*const ()>());
    assert!(size_of::<anyhow::Error>() < size_of::<NotCached>());
    assert_eq!(size_of::<NotCached>(), size_of::<String>());

    // bail! and ensure! produce a one-frame error from a message alone.
    let empty = parse_size("").unwrap_err();
    assert_eq!(empty.to_string(), "manifest is empty");
    assert_eq!(empty.chain().count(), 1);
    assert!(empty.source().is_none());
    assert!(empty.downcast_ref::<io::Error>().is_none());

    let bailed = parse_size("unknown").unwrap_err();
    assert_eq!(bailed.to_string(), r#"size "unknown" is not a number"#);
    assert_eq!(format!("{bailed:#}"), format!("{bailed}"));

    // The message payload's type depends on whether the format string had
    // arguments: the interpolated bail! above stored a String, while anyhow!
    // with a bare literal stores a &'static str. Anyone downcasting a message
    // error has to know which of the two they are asking for.
    assert!(bailed.downcast_ref::<String>().is_some());
    let literal = anyhow!("no sample matched");
    assert!(literal.downcast_ref::<&str>().is_some());
    assert!(literal.downcast_ref::<String>().is_none());

    // ParseIntError converts through the same blanket From.
    assert_eq!(parse_size("4096").unwrap(), 4096);
    let bad_digits = parse_size("40x96").unwrap_err();
    assert!(bad_digits.is::<std::num::ParseIntError>());

    // Context works on Option too — None becomes an error with no source.
    let map: std::collections::HashMap<&str, u64> = std::collections::HashMap::new();
    let absent = map.get("sha256:abc").context("no size recorded").unwrap_err();
    assert_eq!(absent.to_string(), "no size recorded");
    assert_eq!(absent.chain().count(), 1);

    // with_context is lazy: the closure never runs on the happy path.
    let calls = Cell::new(0u32);
    let good: io::Result<u64> = Ok(7);
    let value = good
        .with_context(|| {
            calls.set(calls.get() + 1);
            "never formatted"
        })
        .unwrap();
    assert_eq!(value, 7);
    assert_eq!(calls.get(), 0);

    let bad: io::Result<u64> = Err(io::Error::from(io::ErrorKind::PermissionDenied));
    let _ = bad.with_context(|| {
        calls.set(calls.get() + 1);
        "formatted once"
    });
    assert_eq!(calls.get(), 1);

    // context() takes the message by value, so the argument is built before the
    // Result is even inspected. On a hot path that succeeds, that is a format!
    // per call for a string nobody reads.
    let unused: io::Result<u64> = Ok(11);
    let _ = unused.context({
        calls.set(calls.get() + 1);
        "built on the happy path"
    });
    assert_eq!(calls.get(), 2);

    println!("CONTRACT PASS: context chain intact, {{}} lossy, {{:#}} flattened");
}

Seeder d'origine

anonymous