unlock unlimited code runs for $79, limited time →
NOSSLETTER.techLIVE
back to OSSpedia
rust-lang/rust
senior7/10
error-handlingcompiler-internalstesting-strategy

point at closure return expression in non-FnOnce E0271 errors

Project: rust-lang/rust | Language: Rust | Concepts: error-handling, compiler-internals, testing-strategy | Difficulty: senior
Link: https://github.com/rust-lang/rust/pull/159839

The problem

When a closure is passed as a function argument and a type error (E0271) occurs involving an associated type of the closure's return type—not the output of FnOnce—the compiler's error message did not point at the actual return expression inside the closure. This made it harder for users to identify the source of the type mismatch.

Why it's non-trivial

A naive fix might simply point to the closure itself or the function call, but that doesn't help users find the specific expression causing the type error. The compiler's internal representation makes it tricky to recover the closure's return expression span, especially when the failure is not directly about FnOnceOutput. The code must carefully extract and annotate the right locations to provide actionable diagnostics.

The approach

The main change is in the error reporting logic for trait fulfillment errors, specifically for projection failures involving closure arguments. The PR introduces logic to find and annotate the closure's return expression and label the closure declaration, mirroring existing handling for FnOnce output mismatches.

Key code snippets:

-            let (msg, span, closure_span) = values
+            let (msg, mut span, mut closure_span) = values

This change allows span and closure_span to be mutated, so they can be updated if better information is found.

+            // When the obligation comes from a closure arg and the projection isn't FnOnceOutput
+            // (which maybe_detailed_projection_msg handles via self_ty), point at the closure's
+            // return expression and label the closure declaration.
+            if closure_span.is_none()
+                && let ObligationCauseCode::FunctionArg { arg_hir_id, .. } = obligation.cause.code()
+                && let Node::Expr(arg_expr) = self.tcx.hir_node(*arg_hir_id)
+                && let hir::ExprKind::Closure(closure) = arg_expr.kind
+                && closure.kind == hir::ClosureKind::Closure
+            {
+                let body = self.tcx.hir_body(closure.body);
+                let ret_span = match body.value.kind {
+                    hir::ExprKind::Block(hir::Block { expr: Some(expr), .. }, _) => expr.span,
+                    hir::ExprKind::Block(hir::Block { expr: None, stmts: [.., last], .. }, _) => {
+                        last.span
+                    }
+                    _ => body.value.span,
+                };
+                if !closure.fn_decl_span.overlaps(ret_span) {
+                    closure_span = Some(closure.fn_decl_span);
+                    span = ret_span;
+                }
+            }

This block:

  • Checks if the error is about a closure argument and not already handled by existing logic.
  • Locates the closure's body and attempts to find the span of the return expression.
  • Sets span to the return expression and closure_span to the closure declaration, but only if they don't overlap (to avoid redundant labels).

Testing:

A new test verifies that the error now points at the right place:

needs_unit_item(|| S); //~ ERROR type mismatch resolving `<S as MyTrait>::Item == ()`
needs_unit_item(|| { S }); //~ ERROR type mismatch resolving `<S as MyTrait>::Item == ()`
let x = S;
needs_unit_item(|| { let _ = 0; x }); //~ ERROR type mismatch resolving `<S as MyTrait>::Item == ()`

The corresponding .stderr file shows the error now points at the closure's return expression and labels the closure declaration with "this closure".

From the review thread

in this case the issue is the closure argument, not the return type. While the failure was a projection failure, this is just noise after the main trait goal erroroed above already.

The error doesn't really get better or worse here, but we should somehow avoid emitting two errors


That would be ideal, if it can be done cleanly.

Takeaways

  • When reporting type errors involving closures, pointing to the specific return expression inside the closure helps users quickly identify and fix the problem.
  • Compiler diagnostics should annotate both the cause (the problematic expression) and the context (the closure declaration) for clarity.
  • Error reporting logic often needs to reconstruct context from internal representations—robust handling of AST/HIR nodes is key for actionable diagnostics.