サンプル
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
検証済みサンプル — cargo 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…
sha256:709971847660bb4f230ee832aa66da4263481bd8d5e0a498178ee3b789c53404
このネットワークが提供するのは一つだけです。ビルドされるサンプル。サンドボックスで実行し、署名済みの受領証を保管します。等級はつけず、何も保証しません — 同じコードがあなたの環境でビルドされるかは測定していません。
合格した契約受領証を提出した異なる署名鍵の数です。1 なら作者だけ、2 以上なら他の誰かもビルドしています。鍵は自己生成で背後に登録された身元がないため、数えているのは人ではなく鍵です。
MIT-0
実行証拠
宣言された環境と署名済みの実行を分けてあります。このサンプルが何をどこで実行したかをそのまま確認できます。
- 証拠の基準
- 署名済みコントラクト合格
- 検証レシート
- 3
- ビルドした署名鍵
- 3
宣言された環境
rust 1 linux x64 rust 1 rust cargo
検証実行環境
| 環境 | コントラクト | ステージ | 実行日 |
|---|---|---|---|
| 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 |
ケース
HOW- ゴール
- 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
- シンボル
-
- anyhow::Context::context
- anyhow::Context::with_context
- anyhow::Error::chain
- anyhow::Error::root_cause
- anyhow::Error::downcast_ref
- anyhow::bail
- anyhow::ensure
- 環境
- rust 1
- 作成日
- 2026-08-14T08:53:52Z
コントラクト
- 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 &str
- 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
ファイル
- Cargo.lock
- Cargo.toml
- csx.json
- src/main.rs
ソース
# 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"
[package]
name = "csx-sample-anyhow"
version = "1.0.0"
edition = "2021"
license = "MIT-0"
[dependencies]
anyhow = "1.0.104"
thiserror = "2.0.20"
{"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"}
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");
}