the ? operator

what it does, what it desugars to, and how errors travel up your call stack.

the long way

You already know the shape. Your function returns a Result, it calls something else that returns a Result, and now you have to deal with the variant before you can touch the value. So you wrote this match:

let row = match db.query(id) {
    Ok(r) => r,
    Err(e) => return Err(AppError::from(e)),
};

Nothing wrong with it. It's honest code: check the variant, unwrap the success, early-return the failure. The problem is only that it's the same honest code, every time. Every fallible call in your function needs this exact five-line shape, and after the second one the function stops reading like its own logic and starts reading like error scaffolding with logic squeezed in between.

? compresses that pattern. It doesn't hide it, doesn't change it — it's literally the same match, generated for you.

what ? actually is

When you write expr? on a Result, the compiler sees exactly this:

match expr {
    Ok(v) => v,
    Err(e) => return Err(From::from(e)),
}

Two things in there deserve your attention. First: that return is a real early return — from your function, not from some closure or handler. Which means your function's return type has to be able to hold the error. You can't use ? on a Result inside a function that returns plain Npc; there's nowhere for the Err to go, and the compiler will tell you so.

Second: From::from(e). The error gets run through the From trait on its way out. If your function returns Result<_, AppError> and the call fails with a DbError, and you've written impl From<DbError> for AppError, the conversion happens silently at the ?. That's why one AppError type can absorb failures from a whole stack of libraries — each ? is a conversion point.

Here's the transform, side by side. Then press trace ? and step through what the compiler sees at the first ?:

what you wrote before
fn load_npc(id: u32) -> Result<Npc, AppError> {
    let row = match db.query(id) {
        Ok(r) => r,
        Err(e) => return Err(AppError::from(e)),
    };
    let npc = match Npc::parse(row) {
        Ok(n) => n,
        Err(e) => return Err(AppError::from(e)),
    };
    Ok(npc)
}
what ? lets you write
fn load_npc(id: u32) -> Result<Npc, AppError> {
    let row = db.query(id)?;
    let npc = Npc::parse(row)?;
    Ok(npc)
}
Ok(v)  → unwrap to v, continue Err(e) → From::from(e), return Err

press trace ? to step through what the compiler sees at db.query(id)?.

One more thing while the desugaring is fresh: ? also works on Option, with its own separate expansion — no From, because there's no error to convert:

match expr {
    Some(v) => v,
    None => return None,
}

Same rule about compatibility, though: ? on an Option needs your function to return an Option. You can't mix the two in one function without converting first (ok_or is the usual bridge).

the shape it makes

Look back at the right-hand panel. The happy path flows straight down: query, parse, wrap, done. Every line is a step in the success story. The error handling didn't disappear — each ? is still a full match with an early return in it — but it's been compressed to a single character sitting exactly at the point where things can fail. You can read the function as its logic, and you can still see every place it might bail.

That's the honest way to say it: ? doesn't handle errors, and it doesn't make them disappear. It makes propagation explicit and cheap. The error still has to land somewhere — some function up the stack that matches on it, logs it, retries, whatever. Everything between the failure and that handler just says ?: not my problem, pass it up.

fn tick_day(world)
handles the error here — match, log, recover
└─ load_npc(id)?
? propagates up — doesn't handle, transfers
└─ db.query(id)?
deepest fallible call — errors start here

fire an error in any frame and watch the red token bubble up through each ? until tick_day catches it and it turns green. ? doesn't handle — it transfers.

This is exactly the shape of tick_day in snacko-rs — db reads, NPC state parsing, affinity table lookups, a whole chain of fallible calls in sequence — and ? is what lets that function read as the day's logic instead of the error scaffolding around it.