CodeSampleX

Пример

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

Контракт

  1. assert struct field named cause returns None from source() unless explicitly annotated with #[source]
  2. assert struct field named source implicitly enables source() without #[source]
  3. assert #[error(transparent)] forwards source() to inner error rather than returning inner as wrapper source
  4. assert #[from] error conversion silently loses call-site contextual parameters
  5. assert outer error formatting with {0} causes duplicate messages when walking source chain

Файлы

  • Cargo.lock
  • Cargo.toml
  • NOTES.md
  • csx.json
  • src/lib.rs

Скачать артефакт с исходным кодом (tar.gz)

Исходный код

Cargo.lock
# 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"
Cargo.toml
[package]
name = "cargo-thiserror-source"
version = "0.1.0"
edition = "2021"
license = "MIT-0"

[dependencies]
thiserror = "2.0.20"
NOTES.md
# 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.
csx.json
{"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"}
src/lib.rs
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
    }
}

Исходный сидер

anonymous