What is the best way to ignore a `Result`?

Say I have some code that removes a file:

fn main() {
    delete();
}

fn delete() -> std::io::Result<()> {
    std::fs::remove_file("notes.txt")
}

If I try to compile this code, I get a warning telling me there's a Result that must be used:

warning: unused `std::result::Result` that must be used
 --> src/main.rs:2:5
  |
2 |     delete();
  |     ^^^^^^^^^
  |
  = note: `#[warn(unused_must_use)]` on by default
  = note: this `Result` may be an `Err` variant, which should be handled

Great! But let's say I don't care if an Err is returned. I can suppress the warning by discarding the return value:

    let _ = delete();

From a bit of searching, it seems like this is the recommended way to ignore a Result. However, I recently encountered an issue that's making me rethink this. Say I refactor my program to be asynchronous:

#[tokio::main]
async fn main() {
    let _ = delete();
}

async fn delete() -> std::io::Result<()> {
    tokio::fs::remove_file("wow").await
}

The problem here is that I'm no longer ignoring a Result; I'm ignoring a Future. So the program will exit without awaiting on the Future returned by delete, the file won't be removed, and the compiler won't give me any warning.

I've seen some people use result.ok() instead of let _ = result. This would work better in this example because the compiler would throw an error, complaining that there's no ok method for the Future.

Should I be using Result::ok to ignore a Result? Or is there some way to tell the compiler "hey, don't warn me if there's an unused Result but do warn me if there's some other type that's marked as must_use"?

4 Likes

Besides the .ok() trick, there's no way to opt-out of warnings for only one of them.

This seems like another good argument for adding a Result::ignore_errors method to the standard library.

3 Likes

You could also use e.g. .unwrap_or(()). This also helps ensure that you’re not ignoring anything inside of the result either, but just its error case.


Another option is to use let _: Result<(), _> = delete(); which generalizes to other cases like let _: Result<bool, _> = blah();. This helps ensure that you won’t be missing this spot if anything about delete or blah changes, so you can re-evaluate that you still want to ignore the result.


Edit: Actually, this makes me think, it would be nice to have something like delete() as Result<(), _>; or in the future with type ascription even delete(): Result<(), _>; not create any unused_must_use warnings. I mean, it clearly states “this is a Result-typed value and I’m ignoring it”, doesn’t it?

3 Likes

I wonder if the compiler should also warn on non-awaited or non-polled Futures, with some exceptions for things like spawning separated tasks (could probably add an attribute for those).

2 Likes

This reminds me of a bug I've had to fix recently. I've had a helper function like this:

fn run_task(task: impl Future) -> impl Future {
    tokio::task::spawn(task)
}

and it worked when called like this:

let _ = run_task(do_in_background);

until I changed it to:

async fn run_task(task: impl Future) {
    spawn(task).await;
}

which caused it to silently do nothing.

7 Likes

To play devil's advocate for a bit...

Suppose I have a non-async function that does some work, and then returns a Future that may or may not need to be waited on. It's a little unusual, I'll grant, but not out of the question:

fn foo() -> impl Future<...> {
    // do some stuff here that needs to be done unconditionally
    async {
        // do some stuff here that the caller has to .await if they want it
    }
}

If we warn on let _ = foo(), does the caller now have no way to silence this warning?

Does every must_use type need a bespoke ignore method with a unique name? ignore_error() for Result, dont_await() for Future?

I'm normally a big fan of defensive programming, but I'm not really convinced by the "refactoring" argument tbh, because for any given code you can imagine some refactoring that will create a bug.

What if the refactoring were inside-out? Instead of returning a Future<Result>, now you return a Result<Future>, because the error can actually happen before entering the async block. Tagging on .ok() now doesn't help you at all.

It isn't that unusual, it's the same situation as spawning a concurrent tasks (e.g. with tokion::task::spawn). In fact I already suggested there should be an attribute to mark the future or the function that returns it that allow you to ignore that Future without triggering the warning, basically the opposite of a must_use attribute.

I like how explicit and flexible this is (e.g., I can use Result<(), _> if I want the compiler to raise an error if the Ok type parameter changes or just Result<_, _> if I don't). It's a shame it's so verbose, though. That's the nice thing about Result::ok, though that approach is less clear about what it's doing.

Also, for what it's worth, I found this crate which is pretty neat:

https://crates.io/crates/ignore-result

It just adds Result::ignore (like @mbrubeck suggested) which seems like it checks all the boxes I want: it's relatively terse, it's clear what calling the method does, and the compiler will complain if the return type changes (e.g., to a Future). The only downside I can think of is that there won't be any warnings if one of the type parameters of the Result changes but that's less of a big deal to me because if I don't care if the Result is Ok or Err, I'm unlikely to care what the type parameters are.

Some more (but similarly verbose) options include

let _ = delete() as Result<(), _>;
drop(delete() as Result<(), _>);
drop::<Result<(), _>>(delete());