Пример
thiserror 2.0.20: Unannotated cause fields silently sever Error::source(), #[error(transparent)] delegates rather than wrapping, and #[from] drops call-site context
Проверенный пример — cargo thiserror 2.0.20: Unannotated cause fields silently sever Error::source(), #[error(transparent)] delegates rather than wrapping…
sha256:b162c28cead8cd82c46ab9054fec2a264ef8c956fc40e9aa369447b786e5229c
Эта сеть предлагает одно: образец, который собирается. Она запустила его в песочнице и сохранила подписанную квитанцию. Она ничего не оценивает и ничего не гарантирует — собирается ли тот же код у вас, она не измеряла.
Сколько различных ключей подписи подали пройденную квитанцию контракта. Один — только автор; больше одного — значит, кто-то ещё тоже собрал. Ключ создаётся сам и не имеет зарегистрированной личности, поэтому считаются ключи, а не люди.
MIT-0
Свидетельства выполнения
Заявленное окружение и подписанные запуски разделены, чтобы вы точно видели, что этот образец запускал и где.
- Основа свидетельства
- Подписанный контракт пройден
- Квитанции проверки
- 3
- Ключи подписи, собравшие его
- 3
Заявленная среда
rust linux x64 rust rust cargo
Среды запусков проверки
| Окружение | Контракт | Этапы | Запуск |
|---|---|---|---|
| rust 1 · linux alpine/x64 · docker ed25519:ac1544ece1e22594 | PASS | compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS CONTAINER_RUN · cargo@1 |
2026-08-15 |
| 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- Цель
- Unannotated cause fields silently sever Error::source(), #[error(transparent)] delegates rather than wrapping, and #[from] drops call-site context
- Пакеты
- Символы
-
- thiserror::Error
- std::error::Error::source
- std::convert::From
- Окружение
- rust
- Создан
- 2026-08-15T04:22:49Z
Контракт
- assert struct field named cause returns None from source() unless explicitly annotated with #[source]
- assert struct field named source implicitly enables source() without #[source]
- assert #[error(transparent)] forwards source() to inner error rather than returning inner as wrapper source
- assert #[from] error conversion silently loses call-site contextual parameters
- assert outer error formatting with {0} causes duplicate messages when walking source chain
Файлы
- Cargo.lock
- Cargo.toml
- NOTES.md
- csx.json
- src/lib.rs
Исходный код
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "cargo-thiserror-source"
version = "0.1.0"
dependencies = [
"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 = "cargo-thiserror-source"
version = "0.1.0"
edition = "2021"
license = "MIT-0"
[dependencies]
thiserror = "2.0.20"
# CSX Sample Notes: thiserror Source Chain Resolution and #[from] Context Traps
## Search Known Solution Result
- `search_known_solution` returned a COMPATIBLE hit: sample `sha256:9a1d6344cb2cb3d1c49ea5c0109c80a69519cf2325dc01c5bbc82e5bddcb98cd` (*"Define a library error type with thiserror and keep the source chain intact"*).
- **Why this sample is distinct**: The existing sample demonstrates only basic `#[from]` and `#[error(transparent)]` happy paths. This sample exposes four critical failure modes: (1) fields named `cause`/`inner` format correctly in `Display` but silently return `None` from `Error::source()` unless explicitly annotated with `#[source]`; (2) `#[error(transparent)]` forwards `.source()` to `inner.source()` rather than returning `Some(&inner)`; (3) `#[from]` ergonomics incentivize `?` propagation that silently drops call-site context (e.g. query strings or file paths); and (4) interpolating `{0}` or `{source}` into `#[error("...: {0}")]` creates duplicate message output when diagnostic reporters walk the source chain.
## What a Model Would Have Written Instead
- A model would write `#[error("failed to open {path}: {cause}")] Struct { path: String, cause: io::Error }` expecting `cause` to be recognized as the source, and use `#[from]` on enum variants for convenient `?` propagation while believing call-site context is preserved.
## How the Wrong Version Fails
- It fails silently with a green build: the code compiles and `Display` prints the error string, but `err.source()` returns `None` (breaking downcasting, error tracing in anyhow/tracing/sentry, and root cause analysis) and operation context is discarded at the `?` boundary.
{"case":{"caseId":"case:sha256:b39937df410a2c4329a21a4a96e1c8562d04e29553662fa78ce3cdad0d1fd790","constraints":{"runtime":"rust"},"contract":["assert struct field named cause returns None from source() unless explicitly annotated with #[source]","assert struct field named source implicitly enables source() without #[source]","assert #[error(transparent)] forwards source() to inner error rather than returning inner as wrapper source","assert #[from] error conversion silently loses call-site contextual parameters","assert outer error formatting with {0} causes duplicate messages when walking source chain"],"goal":"Unannotated cause fields silently sever Error::source(), #[error(transparent)] delegates rather than wrapping, and #[from] drops call-site context","kind":"HOW","packages":["pkg:cargo/thiserror@2.0.20"],"schemaVersion":1,"symbols":["thiserror::Error","std::error::Error::source","std::convert::From"]},"contractCommand":["cargo","test","--offline"],"environment":{"arch":"x64","ecosystem":"cargo","executionContext":"rust","language":"rust","os":"linux","packageManager":"cargo","runtime":"rust","schemaVersion":1},"license":"MIT-0","packages":["pkg:cargo/thiserror@2.0.20"],"schemaVersion":1,"symbols":["thiserror::Error","std::error::Error::source","std::convert::From"],"verifierAdapter":"cargo@1"}
use std::io;
use thiserror::Error;
#[derive(Debug, Error)]
#[error("leaf failure: code={code}")]
pub struct LeafError {
pub code: u32,
}
/// TRAP 1: Field name `cause` vs `source` without `#[source]`.
/// `thiserror` ONLY auto-detects fields named exactly `source`.
/// A field named `cause` (or `inner`, `err`) without `#[source]` is ignored by `.source()`.
#[derive(Debug, Error)]
#[error("unannotated cause for path {path}: {cause}")]
pub struct UnannotatedCauseError {
pub path: String,
pub cause: io::Error,
}
#[derive(Debug, Error)]
#[error("annotated cause for path {path}: {cause}")]
pub struct AnnotatedCauseError {
pub path: String,
#[source]
pub cause: io::Error,
}
#[derive(Debug, Error)]
#[error("implicit source for path {path}: {source}")]
pub struct ImplicitSourceError {
pub path: String,
pub source: io::Error, // Name `source` is special in thiserror
}
/// TRAP 2: `#[error(transparent)]` forwards `.source()` to `inner.source()`.
/// Calling `.source()` on a transparent wrapper does NOT return `Some(&Inner)`.
/// If `Inner.source()` is `None`, the wrapper's `.source()` is `None`.
#[derive(Debug, Error)]
pub enum TransparentError {
#[error(transparent)]
Leaf(LeafError),
#[error("non-transparent wrapping")]
Wrapped(#[from] LeafError),
}
/// TRAP 3: `#[from]` enables `?` but cannot carry dynamic call-site context.
/// Context-rich errors must use struct variants with `#[source]` mapped via `.map_err()`.
#[derive(Debug, Error)]
pub enum ServiceError {
#[error("database execution failed")]
FromSwallowsContext(#[from] LeafError),
#[error("query '{query}' on table '{table}' failed")]
ContextPreserved {
query: String,
table: String,
#[source]
source: LeafError,
},
}
/// TRAP 4: Interpolating `{0}` or `{source}` duplicates messages when walking `.source()`.
#[derive(Debug, Error)]
pub enum ChainDisplayError {
#[error("operation failed: {0}")]
Interpolated(#[source] LeafError),
#[error("operation failed")]
Clean(#[source] LeafError),
}
#[cfg(test)]
mod tests {
use super::*;
use std::error::Error as StdError;
#[test]
fn test_unannotated_cause_field_silently_drops_source() {
let err = UnannotatedCauseError {
path: "/etc/config".into(),
cause: io::Error::new(io::ErrorKind::NotFound, "missing file"),
};
// Display includes the cause string because of formatting...
assert_eq!(err.to_string(), "unannotated cause for path /etc/config: missing file");
// ...but source() returns None because the field was named `cause` without #[source]!
assert!(err.source().is_none());
// Explicit #[source] on `cause` fixes source()
let err_annotated = AnnotatedCauseError {
path: "/etc/config".into(),
cause: io::Error::new(io::ErrorKind::NotFound, "missing file"),
};
assert!(err_annotated.source().is_some());
assert_eq!(err_annotated.source().unwrap().to_string(), "missing file");
// Implicitly named `source` field works without #[source] attribute
let err_implicit = ImplicitSourceError {
path: "/etc/config".into(),
source: io::Error::new(io::ErrorKind::NotFound, "missing file"),
};
assert!(err_implicit.source().is_some());
assert_eq!(err_implicit.source().unwrap().to_string(), "missing file");
}
#[test]
fn test_transparent_delegates_source_instead_of_wrapping() {
let leaf = LeafError { code: 404 };
let transparent = TransparentError::Leaf(leaf);
// Display delegates to inner
assert_eq!(transparent.to_string(), "leaf failure: code=404");
// source() delegates to LeafError.source() which is None (NOT Some(&LeafError))
assert!(transparent.source().is_none());
// Contrast: standard #[from] returns Some(&LeafError)
let wrapped = TransparentError::Wrapped(LeafError { code: 500 });
let src = wrapped.source().expect("standard wrapper returns inner");
assert_eq!(src.downcast_ref::<LeafError>().unwrap().code, 500);
}
#[test]
fn test_from_swallows_call_site_context() {
let from_err = (|| -> Result<(), ServiceError> {
Err(LeafError { code: 101 })?; // `?` uses From<LeafError>, dropping SQL query info
Ok(())
})().unwrap_err();
assert_eq!(from_err.to_string(), "database execution failed");
let ctx_err = (|| -> Result<(), ServiceError> {
Err(LeafError { code: 101 }).map_err(|source| ServiceError::ContextPreserved {
query: "SELECT 1".into(),
table: "users".into(),
source,
})?;
Ok(())
})().unwrap_err();
assert_eq!(ctx_err.to_string(), "query 'SELECT 1' on table 'users' failed");
assert_eq!(ctx_err.source().unwrap().downcast_ref::<LeafError>().unwrap().code, 101);
}
#[test]
fn test_chain_traversal_avoids_duplicate_formatting() {
let dup = ChainDisplayError::Interpolated(LeafError { code: 1 });
let clean = ChainDisplayError::Clean(LeafError { code: 1 });
fn walk(mut err: &dyn StdError) -> Vec<String> {
let mut msgs = vec![err.to_string()];
while let Some(s) = err.source() {
msgs.push(s.to_string());
err = s;
}
msgs
}
let dup_msgs = walk(&dup);
assert_eq!(dup_msgs[0], "operation failed: leaf failure: code=1");
assert_eq!(dup_msgs[1], "leaf failure: code=1"); // Duplicated in chain
let clean_msgs = walk(&clean);
assert_eq!(clean_msgs[0], "operation failed");
assert_eq!(clean_msgs[1], "leaf failure: code=1"); // Clean, unique per level
}
}