Skip to content

S08: Ignored Result (let _ =, .ok();)

S08Severity: High

An error is thrown away silently.

Clippy: let_underscore_must_use, unused_result_ok

use std::io::Write;
pub fn write_invoice(out: &mut impl Write, invoice_id: u64, total_cents: u64) {
let _ = writeln!(out, "Invoice {invoice_id}");
let _ = writeln!(out, "Total: {total_cents} cents");
out.flush().ok();
}

Each writeln! and the flush return a Result. let _ = and .ok() throw it away. If the disk is full, the function returns normally and the caller thinks that the invoice exists.

  • The compiler warns about an unused Result (“unused Result that must be used”). let _ = is the fastest way to stop the warning.
  • The function signature has no Result, so ? does not compile. The agent ignores the error instead of changing the signature.
  • Silent data loss. Writes fail for real reasons: a full disk, a closed socket, a lost network drive. The program continues as if the data were saved.
  • Hard to debug. Nothing is logged, and there is no error to follow. The problem shows up later, far from its cause.
  • It hides the intent. A reader cannot tell if the author ignored the error on purpose or by accident.

Make the function return a Result and propagate each error with ?:

  1. WorseEvery write error is discarded. The caller always sees success.

    use std::io::Write;
    pub fn write_invoice(out: &mut impl Write, invoice_id: u64, total_cents: u64) {
    let _ = writeln!(out, "Invoice {invoice_id}");
    let _ = writeln!(out, "Total: {total_cents} cents");
    out.flush().ok();
    }
  2. BetterReturns io::Result<()>. A failed write reaches the caller.

    use std::io::{self, Write};
    pub fn write_invoice(
    out: &mut impl Write,
    invoice_id: u64,
    total_cents: u64,
    ) -> io::Result<()> {
    writeln!(out, "Invoice {invoice_id}")?;
    writeln!(out, "Total: {total_cents} cents")?;
    out.flush()
    }

The last line, out.flush(), has no ;, so its io::Result<()> is the return value. See Result, not exceptions.

Sometimes an error really does not matter, for example when a best-effort clean-up fails. Then make the decision visible: handle the error explicitly, log it, and write a comment.

pub fn remove_temp_file(path: &Path) {
// Best effort: the OS cleans the temp directory later. A failure
// here does not change the result, so we ignore it on purpose.
if let Err(err) = fs::remove_file(path) {
eprintln!("warning: could not remove {}: {err}", path.display());
}
}

A bare let _ = ...; with a comment that says why is also acceptable. Without the comment, it is a question for the agent.

  • Search for let _ = and .ok(); (a statement that ends right after .ok()). Also look for .unwrap_or_default() and .unwrap_or(()) on operations that can fail.
  • Clippy let_underscore_must_use and unused_result_ok (both in the restriction group, off by default) flag these patterns.
  • The compiler lint unused_must_use (on by default) catches a Result that is not used at all. let _ = exists to silence it, so it does not catch this smell.
In write_invoice, do not discard errors. Change the return type to
io::Result<()> and use ? on each writeln!, and return out.flush(). Update the
callers to handle the error. If you intentionally ignore an error anywhere,
log it and add a comment that says why.

Related: S02: unwrap() / expect() in non-test code.