An alert lands. You were eight hours intensely into something completely unrelated to that alert. You have this alert now, and you are not just tired. You are spent.

The log says the sequencer failed. You open the crate that returns EngineError. You search for EngineError::Sequencer.

Zero call sites.

The conversion happened. The compiler was happy. You are not. The ? that made some method in that engine code look clean is the line that hides what you cannot find when the unexpected alert hits you right after you emptied the tank on a problem that had nothing to do with this matching engine.

Many things push you to use it. From is idiomatic. The book teaches it, and thiserror will derive it for you if you put #[from] on a variant. Then ? does what people hired Rust for: in the most compact way for the codebase, the inner error becomes the outer error and the function body stays a list of happy-path calls.

The ecosystem rewards this. Less typing. One impl From per variant, generated, correct, and erased in the sense that you never write the wrap. The standard library is full of From. String from &str. io::Error from a long list of OS failures. Conversion is the language’s way of saying this is that.

If you object, you can feel someone appeal to brevity and paint you as wanting ceremony for the sake of it.

But in that case, that someone is telling you the compiler already inlined the From impl while you are arguing about a line helpful for diagnosing, a line coded for understanding, a line that does not survive codegen. They are thinking in cycle costs. Chances are they are pointing at the wrong bill.

The bill is not paid when the binary runs. It is paid when you are exhausted, the alert is unrelated to the work you just did, and the wrap has no name you can search.

In that prod alert, which function wrapped EngineError::Sequencer?

#[derive(Debug, Error)]
enum EngineError {
    #[error(transparent)]
    Config(#[from] ConfigError),
    #[error(transparent)]
    Sequencer(#[from] sequencer::Error),
}

fn start() -> Result<(), EngineError> {
    let config = Config::load()?;
    sequencer::bind()?;
    Ok(())
}

fn ingest(command: EngineCommand) -> Result<(), EngineError> {
    sequencer::enqueue(command)?;
    Ok(())
}

fn recover(from: Sequence) -> Result<(), EngineError> {
    sequencer::replay(from)?;
    Ok(())
}

In that snippet we have three, so it is tempting to shrug because the number is low. That thinking scales poorly. At 10 or 20 you get exponential disambiguation costs.

That friction to diagnose is a causality cost. #[from] hides it for cleanness. You got the cleaner source. You are paying for it in missing origin evidence.

I already made that argument for extracting a function. Named calls are usually free. This is the sibling intuition, just pointed at where errors happen: do not add a line; ? is enough, cleaner, more compact. Same reflex as “saving an extra call.”

But the site is the evidence we need to find fast and under pressure.

? still sits on a source line. A backtrace, if you have one, will often point at Config::load()?. Production does not always give you that backtrace. Operators get a Display string. Teammates get a git grep. AI agents get a constructor they can search.

#[from] puts the only EngineError::Sequencer(...) in a derived From impl you do not read. The call sites are just ?. Find-references on the variant finds the enum. It does not find the three places (or 20) that actually wrapped a sequencer::Error. You cannot tell a sequencer bind from a config miss without opening every callee and checking its Err type. The function body flattened them into the same punctuation.

If we start unpacking it, using .map_err(EngineError::from)? gives us the same hide with extra steps. You wrote from and threw away the variant name on purpose.

The alternative is to use the symbol to mark the spot: name that variant at the conversion point.

#[derive(Debug, Error)]
enum EngineError {
    #[error("configuration error: {0}")]
    Config(#[source] ConfigError),
    #[error("sequencer error: {0}")]
    Sequencer(#[source] sequencer::Error),
}

fn start() -> Result<(), EngineError> {
    let config = Config::load().map_err(EngineError::Config)?;
    sequencer::bind().map_err(EngineError::Sequencer)?;
    Ok(())
}

fn ingest(command: EngineCommand) -> Result<(), EngineError> {
    sequencer::enqueue(command).map_err(EngineError::Sequencer)?;
    Ok(())
}

fn recover(from: Sequence) -> Result<(), EngineError> {
    sequencer::replay(from).map_err(EngineError::Sequencer)?;
    Ok(())
}

#[source] keeps the chain. .map_err(EngineError::Sequencer) is the line you will search for. It is also the line you can breakpoint. It is also helping you explain the rule in a sentence: this is where a sequencer failure becomes an engine failure, which is a reminder of what job your program was hired for.

That sentence does not belong in a derive. It belongs where the program decides.

The same rule holds at every layer of your application. With one coding convention, the whole program is much more diagnosable when it is in operations. The site you can walk is the constructor, so you will find it fast and make sense of that alert without friction.

And the runtime cost is zero.

Someone will still see .map_err and think you added a call. ? already wraps. That is the whole trick.

result? in a function that returns Result<T, EngineError> desugars to From::from on the inner error. A #[from] on Sequencer generates an impl whose body is EngineError::Sequencer(inner). .map_err(EngineError::Sequencer) is that same constructor, written where the program decides. .map_err(EngineError::from) is the hide with the extra word from.

So the three forms are the same wrap. You can check it the same way as the indirection post: #[no_mangle], optimized assembly, look.

impl From<SequencerError> for EngineError {
    fn from(inner: SequencerError) -> Self {
        EngineError::Sequencer(inner)
    }
}

#[no_mangle]
pub fn wrap_question(result: Result<(), SequencerError>) -> Result<(), EngineError> {
    result?;
    Ok(())
}

#[no_mangle]
pub fn wrap_map_err(result: Result<(), SequencerError>) -> Result<(), EngineError> {
    result.map_err(EngineError::Sequencer)?;
    Ok(())
}

#[no_mangle]
pub fn wrap_from(result: Result<(), SequencerError>) -> Result<(), EngineError> {
    result.map_err(EngineError::from)?;
    Ok(())
}
rustc --edition 2021 -O --crate-type lib --emit asm lib.rs

I compiled a two-variant EngineError (Config and Sequencer) on aarch64. LLVM emitted one function and aliased the other two names to it. Your mnemonics may differ. The aliasing is the point:

_wrap_from:
        mov     w8, #2
        sub     x0, x8, x0
        ret

_wrap_map_err = _wrap_from
_wrap_question = _wrap_from

The wrap itself is a discriminant tweak. So the delta of writing .map_err(EngineError::Sequencer) instead of ? plus From is not “small.” It is the same machine code. Folks are arguing about a distinction that does not survive compilation.

A green compile on the #[from] version proves the From impls exist. It does not prove you will be able to walk the failure.

Consistency is cheap. Locating the wrap is the expensive half, and it is a source location problem. You still use ?. You are refusing to let the type system narrate a wrap that humans have to reconstruct later, at the worst moment, for a human cost that is certainly not zero.

If three calls all produce sequencer::Error, all three conversions still use EngineError::Sequencer. The constructor at the call site still gives you three hits instead of none. That is enough to start addressing that alert. If bind, enqueue, and replay were not in the flow where the alert happens, there is no ambiguity. You know it comes from a different site, without diagnosing delays.

Ban From on error types at the boundary you control. Keep From for values that are actually the same idea in a different suit. Price from a tick count is a conversion. EngineError from SequencerError is a judgment.

The appeal to brevity is the costly one. It saves a constructor name and makes you pay later, when you cannot search. The appeal to a faster runtime if you keep From on the variant does not exist.

Adopt #[source] on your error variants plus the named variant at the call site: one motion, every crate you own, causality you can grep by design.

Write that judgment while you still have a full tank. The next alert will not wait for a fresh mind.

When it lands, you will search for EngineError::Sequencer again and understand the full flow. In no time you will know all the details, and what to do, before anyone asks what happened to the service.