owned this note
owned this note
Published
Linked with GitHub
# How much context for async error traces
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.
## Simple example: one step
Consider this example ([playground](https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=026997547909cf9f9f68a0c38dc2dbaf)):
```rust
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:
```rust
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 nothing
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](https://github.com/rust-lang/rust/pull/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](https://github.com/rust-lang/rust/blob/25c30357d45435fae58ebcb50426682658617818/src/test/ui/async-await/nested-async-calls.rs), we get [output like this](https://raw.githubusercontent.com/rust-lang/rust/25c30357d45435fae58ebcb50426682658617818/src/test/ui/async-await/nested-async-calls.stderr):
```
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
I'd like to know which of these options you prefer, of course, as well as other alterantives that we may not have considered. But 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