One of the challenges of the current async await design is that a requirement that a future must be Send
often comes at some top-level function, where the actual problem that prevents the future from being Send
occurs in some other function. We're struggling a bit at how much information to present and how in order to best explain what's going on. This thread explores a particular example and some possible errors. There is a poll at the end!
Simple example: one step
Consider this example (playground):
fn is_send<T: Send>(t: T) { }
fn main() {
is_send(async_fn1(generate()));
}
async fn async_fn1(future: impl Future + Send) {
let x = Mutex::new(22);
let data = x.lock().unwrap();
future.await;
}
async fn generate() {
}
Currently, it gives the following error:
error: future cannot be sent between threads safely
--> src/main.rs:9:5
|
6 | fn is_send<T: Send>(t: T) { }
| ------- ---- required by this bound in `is_send`
...
9 | is_send(async_fn1(generate()));
| ^^^^^^^ future returned by `async_fn1` is not `Send`
|
= help: within `impl std::future::Future`, the trait `std::marker::Send` is not implemented for `std::sync::MutexGuard<'_, i32>`
note: future is not `Send` as this value is used across an await
--> src/main.rs:15:5
|
14 | let data = x.lock().unwrap();
| ---- has type `std::sync::MutexGuard<'_, i32>`
15 | future.await;
| ^^^^^^^^^^^^ await occurs here, with `data` maybe used later
16 | }
| - `data` is later dropped here
As the error explains, the problem is reported in the main
function, but it's caused by the code in async_fn1
. I think this error is pretty decent, though I'm definitely open to suggestions on how to improve this case.
Example with multiple steps
Now consider this case (playground). The only difference is that main
calls async_fn3
, which in turn calls async_fn2
, and ultimately async_fn1
, which has the problem:
fn is_send<T: Send>(t: T) { }
fn main() {
is_send(async_fn3(generate()));
}
async fn async_fn3(future: impl Future + Send) {
async_fn2(future).await;
}
async fn async_fn2(future: impl Future + Send) {
async_fn1(future).await;
}
async fn async_fn1(future: impl Future + Send) {
let x = Mutex::new(22);
let data = x.lock().unwrap();
future.await;
}
async fn generate() {
}
The error we report in this case is largely unchanged:
error: future cannot be sent between threads safely
--> src/main.rs:9:5
|
6 | fn is_send<T: Send>(t: T) { }
| ------- ---- required by this bound in `is_send`
...
9 | is_send(async_fn3(generate()));
| ^^^^^^^ future returned by `async_fn3` is not `Send`
|
= help: within `impl std::future::Future`, the trait `std::marker::Send` is not implemented for `std::sync::MutexGuard<'_, i32>`
note: future is not `Send` as this value is used across an await
--> src/main.rs:23:5
|
22 | let data = x.lock().unwrap();
| ---- has type `std::sync::MutexGuard<'_, i32>`
23 | future.await;
| ^^^^^^^^^^^^ await occurs here, with `data` maybe used later
24 | }
| - `data` is later dropped here
The concern
There are two concerns with the error messages here:
- They are complex and trying to pack in and explain a lot of stuff.
- Also, with our current reporting, we show only the point of the error (
main
) and the leaf function (async_fn1
) that caused the error. Users have to intuit the path between them.
There is a bit of a trade-off here that we are trying to decide how to resolve. Adding more information makes the message more complex and foreboding, but leaving it out means that users have to figure out.
How to resolve this?
There are a few thoughts on how to resolve this.
Do not show stack trace details (i.e., status quo)
We could keep the status quo. After all, if you want to fix the bug, almost always you will do so by altering the code in the leaf function, so it's not that interesting to find the path from the cause of error to the leaf function, and it's usually not that hard to find. @sfackler expressed this opinion in the past (not to put words in their mouth).
Give the full stack trace
At the other extreme, PR #67116 proposed to alter our reporting in these "multi-step" to include the full details for each step along the way. So, for a very similar example, we get output like this:
error[E0277]: future cannot be sent between threads safely
--> $DIR/nested-async-calls.rs:26:5
|
LL | fn require_send<T: Send>(_val: T) {}
| ------------ ---- required by this bound in `require_send`
...
LL | require_send(wrapped);
| ^^^^^^^^^^^^ future returned by `first` is not `Send`
|
= help: within `main::Wrapper<impl std::future::Future>`, the trait `std::marker::Send` is not implemented for `*const ()`
= note: required because it appears within the type `third::{{closure}}#0::NotSend`
note: future is not `Send` as this value is used across an await
--> $DIR/nested-async-calls.rs:17:5
|
LL | let _a: Outer;
| -- has type `third::{{closure}}#0::Outer`
LL | dummy().await;
| ^^^^^^^^^^^^^ await occurs here, with `_a` maybe used later
LL | }
| - `_a` is later dropped here
note: future is not `Send` as this value is used across an await
--> $DIR/nested-async-calls.rs:8:5
|
LL | third().await;
| -------^^^^^^- `third()` is later dropped here
| |
| await occurs here, with `third()` maybe used later
| has type `impl std::future::Future`
note: future is not `Send` as this value is used across an await
--> $DIR/nested-async-calls.rs:4:5
|
LL | second().await;
| --------^^^^^^- `second()` is later dropped here
| |
| await occurs here, with `second()` maybe used later
| has type `impl std::future::Future`
= note: required because it appears within the type `impl std::future::Future`
= note: required because it appears within the type `main::Wrapper<impl std::future::Future>`
error: aborting due to previous error
For more information about this error, try `rustc --explain E0277`.
Minimal notes
A middle ground might be to note the functions in the stack trace, without giving full details (see the final "note" entries at the end), although we might want to show line numbers or some bit of more information as well:
error[E0277]: future cannot be sent between threads safely
--> $DIR/nested-async-calls.rs:26:5
|
LL | fn require_send<T: Send>(_val: T) {}
| ------------ ---- required by this bound in `require_send`
...
LL | require_send(wrapped);
| ^^^^^^^^^^^^ future returned by `first` is not `Send`
|
= help: within `main::Wrapper<impl std::future::Future>`, the trait `std::marker::Send` is not implemented for `*const ()`
= note: required because it appears within the type `third::{{closure}}#0::NotSend`
note: future is not `Send` as this value is used across an await
--> $DIR/nested-async-calls.rs:17:5
|
LL | let _a: Outer;
| -- has type `third::{{closure}}#0::Outer`
LL | dummy().await;
| ^^^^^^^^^^^^^ await occurs here, with `_a` maybe used later
LL | }
| - `_a` is later dropped here
= note: `third()` is called from `second()`
= note: `second()` is called from `first()`
= note: `first()` is wrapped within the type within the type `main::Wrapper<impl std::future::Future>`
error: aborting due to previous error
For more information about this error, try `rustc --explain E0277`.
Give user control
We don't presently have an option to request verbose errors. We could add this. I am somewhat skeptical -- I think most folks won't know it exists, and I buy into the idea that we should try to tune the defaults to be as useful as we can (without being overwhelming) and avoid adding a lot of knobs. Knobs in particular feel like they will allow us to expend less effort on the defaults and -- since most folks won't use them -- the overall quality of our errors goes down.
What I would like from you
Well, first and foremost, there is a poll in the next section to indicate which of the options you prefer. But you may also have ideas we've not considered! I'm particularly interested in feedback from:
- people who are using async-await frequently
- specific examples of code where having the full backtrace would have been useful
- or cases where you encountered the current (minimal) error and felt confused because it was hard to connect the two points of error
- any suggestions on how we might improve the "core error" as well
Poll
To help simplify things, let's have a poll! Here are the choices that I described above. Vote for as many as you would like to indicate what you prefer. For the purpose of this poll, I've left off the "verbose flag" option. I'd like to know what people think we should do if we don't add any flags. (OK fine, if you really want to vote for a verbose flag, I added an option for that But please also select one of the other choices too)
- Do not show stack trace details
- Show full stack trace details
- Show minimal stack trace details
- Other (add a comment below)
- I can't help it, I really really want a verbose flag. But I've also selected one of the other options.
0 voters