Let _ : () = blah_blah_blah.await?;

I find myself frequently writing let _: () = blah_blah_blah.await?; and I am wondering if this is a code smell.

  1. So starting out, I write:
    blah_blah_blah
    but this code does nothing, because it returns a Future.

  2. Therefore, we write blah_blah_blah.await

  3. Now, because async can have errors all over the place (trying to send, but can't; trying to receive, but empty), the above returns a Result<T, E> and we want to eject on failure, so now we have: blah_blah_blah.await?

  4. Now, not all async fns actually return something useful. Some funcs, like send, just return (), to indicate the send was successful, so now we do:
    let _ : () = blah_blah_blah.await?;

  5. The above, when repeated many times, looks like a code smell of some sort.

Advice ?

If a fn returns a Result<(), ...> and you use ? to return the error, there is no need for the "let _ = ...", right?

2 Likes

I actually write a lot of let () = ...; statements (with or w/o .await), and I find it quite nice to let it be known "nothing of value was lost" in that statement. So, on the contrary, as with any type annotation, you're just making your code mode explicit and, imho, readable!


Similarly, and especially in unsafe code, whenever I'm calling a diverging function, I write

if ptr.is_null() {
    enum Diverged {}
    let _: Diverged = alloc::handle_alloc_error();
}

to make sure the compiler guarantees that function does indeed diverge.

  • (If Infallible hadn't had that ridiculously ill-fitted name (Result<Infallible, ()> is actually not only fallible, but actually doomed to fail, etc.) and had instead been called Unreachable, then I might have used that one rather than an ad-hoc empty enum. I guess we'll have to wait for never sigil !)

Finally, once we get used to making it explicit the intent of discarding values, I find that writing #![deny(unused_must_use)] becomes quite handy :slightly_smiling_face:

1 Like

The danger here is that if I write

blah_blah_blah.await?;

and then I forget the .await?, I get

blah_blah_blah

which does not do what I want it to do.

The let _ : () = ... forces me to ensure that all the asyncs have been awaited.

====

EDIT: the above is not a hypothetical; I have lost hours of my life debugging forgotten .await

1 Like

I don't understand. If you forget the .await, won't the compiler tell you that you didn't use the Future?

2 Likes

This may be a flaw in my development process, but I generally have 10-20 warnings flying around due to development churn (comment out this function, and now suddenly there are lots of unused exports, dead code, unused function), to the point I generally only fix them when the code works.

1 Like

Note that you can #![deny(unused_results)] to require that discarding values is explicit everywhere: https://doc.rust-lang.org/rustc/lints/listing/allowed-by-default.html#unused-results

4 Likes

I was scracthing my head since I vaguely remembered there being such a lint. That one is a bit too much, maybe, although for a codebase where I'm alone I might try enabling it

Is there a way to promote certain warnings to errors ? I would be happy with rustc promoting 'unused future' from warning to error.

1 Like

Yeah, that's why it's allow-by-default.

I do hope that one day we'll get a middle-ground lint -- and maybe clippy already has one -- for things like "hey, that method only takes &s to no-interior-mutability things; maybe don't call it if you don't need the result?" without needing to #[must_use] everything.

1 Like

Using #![deny(lint_name)] or #![forbid(lint_name)] (depending on whether you want to let more fine-grained interior modules undo the deny with an [allow(lint_name)] or [warn(lint_name)]) at the root of the src/lib.rs is the usual way to do that; you can also just do it in a fine-grained / per-module or function basis with #[deny(...)].

Hence my:

#![deny(unused_must_use)]

async fn async_fn() -> Result<(), ()> { Ok(()) }

#[::tokio::main]
async fn main() {
    async_fn(); // Error
    async_fn().await; // Error
    async_fn().await.unwrap(); // OK
    let _ = async_fn().await; // OK ("explicitly" discarded)
}

yielding:

error: unused implementer of `Future` that must be used
 --> src/main.rs:7:5
  |
7 |     async_fn(); // Error
  |     ^^^^^^^^^^^
  |
note: the lint level is defined here
 --> src/main.rs:1:9
  |
1 | #![deny(unused_must_use)]
  |         ^^^^^^^^^^^^^^^
  = note: futures do nothing unless you `.await` or poll them

error: unused `Result` that must be used
 --> src/main.rs:8:5
  |
8 |     async_fn().await; // Error
  |     ^^^^^^^^^^^^^^^^^
  |
  = note: this `Result` may be an `Err` variant, which should be handled

That lint looks really useful. I think I might start using it. :slight_smile:

My go-to for discarding a Result is calling ok() on it.

Subjectively, I like it better than non-binding let:

#![deny(unused_must_use)]

async fn async_fn() -> Result<(), ()> { Ok(()) }

#[::tokio::main]
async fn main() {
    async_fn().await.ok(); // OK
}

.ok() turns a Result into an Option. It's useful, but it's not the behavior the OP is talking about, which is bubbling the error case with ?, and ensuring the success type is unimportant (())

They were talking of doing:

fn emit_smth() -> Result<(), …> { … }

fn log_stuff() -> (/* must be infallible */) {
    // This triggers the `unused_must_use` lint:
    emit_smth();
    // To silence it, rather than doing:
    let _ = emit_smth();
    // parasyte was mentioning that they preferred doing:
    emit_smth().ok(); // <- Does not trigger `unused_must_use`!
}

I personally like the approach, but I'm not super convinced by .ok() being the right method for this (although if enough people do it, it would become, by sheer usage, idiomatic / meaningful; in the same way as doing let _ currently is). I'd love to see a .ignore_err() method being used instead:

emit_smth().ignore_err();
2 Likes

You can write it as

let () = ...

I've always considered this to be bad practice and would always prefer that people used let _ = instead.

4 Likes

That is fair. I don't remember where I picked this up, not that it matters. The way it reads seems intuitive enough, "this result is ok [to ignore]".

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.