Change:
// If this `if` expr is the parent's function return expr, // the cause of the type coercion is the return type, point at it. (#25228) let hir_id = self.tcx.hir().parent_id(self.tcx.hir().parent_id(then_expr.hir_id)); <del> let ret_reason = self.maybe_get_coercion_reason(hir_id, span); let cause = self.cause(span, ObligationCauseCode::IfExpressionWithNoElse); </del> <ins> let ret_reason = self.maybe_get_coercion_reason(hir_id, if_span); let cause = self.cause(if_span, ObligationCauseCode::IfExpressionWithNoElse); </ins> let mut error = false; coercion.coerce_forced_unit( self, &cause, <del> |err| { if let Some((span, msg)) = &ret_reason { err.span_label(*span, msg.clone()); } else if let ExprKind::Block(block, _) = &then_expr.kind && let Some(expr) = &block.expr { err.span_label(expr.span, "found here"); } err.note("`if` expressions without `else` evaluate to `()`"); err.help("consider adding an `else` block that evaluates to the expected type"); error = true; }, </del> <ins> |err| self.explain_if_expr(err, ret_reason, if_span, cond_expr, then_expr, &mut error), </ins> false, ); error } <ins> /// Explain why `if` expressions without `else` evaluate to `()` and detect likely irrefutable /// `if let PAT = EXPR {}` expressions that could be turned into `let PAT = EXPR;`. fn explain_if_expr( &self, err: &mut Diagnostic, ret_reason: Option<(Span, String)>, if_span: Span, cond_expr: &'tcx hir::Expr<'tcx>, then_expr: &'tcx hir::Expr<'tcx>, error: &mut bool, ) { if let Some((if_span, msg)) = ret_reason { err.span_label(if_span, msg.clone()); } else if let ExprKind::Block(block, _) = then_expr.kind && let Some(expr) = block.expr { err.span_label(expr.span, "found here"); } err.note("`if` expressions without `else` evaluate to `()`"); err.help("consider adding an `else` block that evaluates to the expected type"); *error = true; if let ExprKind::Let(hir::Let { span, pat, init, .. }) = cond_expr.kind && let ExprKind::Block(block, _) = then_expr.kind // Refutability checks occur on the MIR, so we approximate it here by checking // if we have an enum with a single variant or a struct in the pattern. && let PatKind::TupleStruct(qpath, ..) | PatKind::Struct(qpath, ..) = pat.kind && let hir::QPath::Resolved(_, path) = qpath { match path.res { Res::Def(DefKind::Ctor(CtorOf::Struct, _), _) => { // Structs are always irrefutable. Their fields might not be, but we // don't check for that here, it's only an approximation. } Res::Def(DefKind::Ctor(CtorOf::Variant, _), def_id) if self .tcx .adt_def(self.tcx.parent(self.tcx.parent(def_id))) .variants() .len() == 1 => { // There's only a single variant in the `enum`, so we can suggest the // irrefutable `let` instead of `if let`. } _ => return, } let mut sugg = vec![ // Remove the `if` (if_span.until(*span), String::new()), ]; match (block.stmts, block.expr) { ([first, ..], Some(expr)) => { let padding = self .tcx .sess .source_map() .indentation_before(first.span) .unwrap_or_else(|| String::new()); sugg.extend([ (init.span.between(first.span), format!(";n{padding}")), (expr.span.shrink_to_hi().with_hi(block.span.hi()), String::new()), ]); } ([], Some(expr)) => { let padding = self .tcx .sess .source_map() .indentation_before(expr.span) .unwrap_or_else(|| String::new()); sugg.extend([ (init.span.between(expr.span), format!(";n{padding}")), (expr.span.shrink_to_hi().with_hi(block.span.hi()), String::new()), ]); } // If there's no value in the body, then the `if` expression would already // be of type `()`, so checking for those cases is unnecessary. (_, None) => return, } err.multipart_suggestion( "consider using an irrefutable `let` binding instead", sugg, Applicability::MaybeIncorrect, ); } } </ins> pub fn maybe_get_coercion_reason( &self, hir_id: hir::HirId,

Discussion:
The following code () gives the following error: which is weird, since the clause would never be visited even if it was present. The equivalent one-arm match compiles just fine (expectedly). Adding an branch to the is, of course, possible, but unelegant, and really shouldn't be needed, as it can be inferred at compile time.
You can match irrefutably without in this case: though maybe we should give this special diagnostics.
I think this is too niche to deserve a special diagnostics (and I don't think we should substantially work on diagnostics for until let_chains is implemented). is defined by a desugaring: becomes: so what you really wrote is: The block is typed at which does not unify with unless it is also typed at .
I didn't know was a valid syntax. If I did, I probably wouldn't have filed the issue, because of course, in hindsight, that's how you write an infallible . (With that knowledge in mind) I agree that it's only a diagnostics issue and no language change is needed here. And I fully agree that it's a niche low-priority quirk that should maybe be fixed "someday". (I don't know what but even then I'd guess that this one is lower priority than that).
I think the appropriate thing to do here would be to lint on , which would have clarified things.
We already do.
But we do only after typechecking, and we don't if there are any errors before then. Quite a few lints have similar issues related to not happening early enough.
How do you know that the pattern is irrefutable before match checking in HAIR?
You can't, but you can write the lint in such a way that it triggers regardless. We're getting to typechecking, we should be able to detect irrefutability by then. It just can't use the standard linting machinery (like a few other lints already do to avoid being silenced by accident).