Skip to content

consider an annotation to tag async members as not needing await #46218

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
pq opened this issue Jun 2, 2021 · 69 comments
Closed

consider an annotation to tag async members as not needing await #46218

pq opened this issue Jun 2, 2021 · 69 comments
Assignees
Labels
area-devexp For issues related to the analysis server, IDE support, linter, `dart fix`, and diagnostic messages. devexp-pkg-meta Issues related to package:meta P3 A lower priority bug or feature request type-enhancement A request for a change that isn't a bug

Comments

@pq
Copy link
Member

pq commented Jun 2, 2021

Follow-up from conversation in dart-lang/core#742.

The idea is to add an annotation that allows API authors to mark declarations as not requiring an await at call sites. This could greatly reduce the number of uses of functions like unawaited to silence the unawaited_futures lint.

@pq pq added area-pkg Used for miscellaneous pkg/ packages not associated with specific area- teams. type-enhancement A request for a change that isn't a bug pkg-meta labels Jun 2, 2021
@bwilkerson
Copy link
Member

Do we want something fairly specific, like awaitNotRequired, or something more general, like doesNotThrow? (My understanding is that we care about un-awaited futures because of exception handling.) I'd guess the former, but thought it worth asking the question.

@pq
Copy link
Member Author

pq commented Jun 2, 2021

Excellent question. My gut says we want the specificity of something like awaitNotRequired but would be very curious to get feedback from folks more familiar with APIs they'd like to see annotated.

/cc @davidmorgan @jefflim-google @goderbauer

@goderbauer
Copy link
Contributor

In another thread where this was discussed (#58348) Hixie suggested @cannotThrow to emphasize that it's only ok to not await Futures if they don't throw. I am also leaning a little more in this direction.

@bwilkerson
Copy link
Member

Which leads to the next question: are there any static checks associated with cannotThrow (other than it can only be applied to functions, methods, getters, and fields)?

For example, I could imagine checking some or all of the following:

  • Functions marked with cannotThrow can't contain either a throw or rethrow.
  • Functions marked with cannotThrow can only invoke functions that are also annotated with it.
  • Overrides of such methods are required to conform to the same contract (that is, it's inherited).

@davidmorgan
Copy link
Contributor

I think order of execution is more of a concern with missing 'await' than exception handling? At least, in web and web test code that was the concern. I don't remember anything specifically about exception handling.

So what I was expecting is:

  • Methods that have a side effect you likely want to wait for, should be either awaited or unawaited
  • Async methods with a side effect you likely don't want to wait for, e.g. logging, could be annotated to silence the lint
  • Async methods you never need to wait for can return void

and for that awaitNotRequired sounds good.

I'm not sure about the requirement to not throw; it seems to me like it's a valid design to have unawaited futures that throw, and rely on the zone exception handler to catch them?

@davidmorgan
Copy link
Contributor

Also, it's quite possible that you have a method that can never throw that you always want to await, e.g.

@cannotThrow
Future<void> sleepABit() => await Future.delayed(Duration(seconds: 2));

So I don't think that works by itself.

@eernstg
Copy link
Member

eernstg commented Jun 3, 2021

Just noted this issue, and I can't help mentioning a thing: A fire-and-forget async function can be expressed by using the return type void. Wouldn't that do the job?

void fireAndForget() async { ... }

void main() async {
  fireAndForget(); // No complaints about unawaited futures.
}

@pq
Copy link
Member Author

pq commented Jun 3, 2021

Thanks @eernstg! I've wondered this too and it's exactly why I think some specific API examples would be handy.

@pq
Copy link
Member Author

pq commented Jun 3, 2021

In dart-lang/core#742, @goderbauer mentioned

https://api.flutter.dev/flutter/animation/AnimationController/forward.html

In this and related cases a void return doesn't allow for the optional use of a returned TickerFuture.

@goderbauer
Copy link
Contributor

These methods return Futures because sometimes you do want to await them (although that's rare), but in the most common cases you should be able to just drop them on the floor without having to litter your code with // ignores or unawaited.

@pq
Copy link
Member Author

pq commented Jun 3, 2021

@goderbauer: I'm curious how you'd feel about adopting an @awaitNotRequired annotation in Flutter APIs. This doesn't preclude @cannotThrow but given @davidmorgan's comments it seems like it alone would only cover some of the bases.

@goderbauer
Copy link
Contributor

@awaitNotRequired sounds good to me. Would be interesting to apply that to the framework methods we know and see how many more unawaited_future warnings remain.

@jacob314
Copy link
Member

jacob314 commented Jun 3, 2021

I would like to see @awaitNotRequred adopted by some dart:sdk apis as well.

For example: Map.remove called on a Map<String,Future> does not need to be awaited as the return value is rarely useful.
Marking this method as @awaitNotRequired does seem odd as the Map class doesn't have anything specifically to do with Futures. Another option is to only trigger the unawaited_futures lint if the return value of a member is Future ignoring the generic type arguments.

@pq
Copy link
Member Author

pq commented Jun 4, 2021

See also: #58348.

@pq
Copy link
Member Author

pq commented Jun 4, 2021

I would like to see @awaitNotRequred adopted by some dart:sdk apis as well.

/fyi @lrhn @leafpetersen @natebosch @jakemac53

If this is something we agree we want to do, it will suggest the annotation living in core (not package:meta).

EDIT: pragmatically, if there are only a few core library declarations we want to tag, we could bake that awareness into the lint and sidestep the challenge of landing a new annotation in the SDK. (Not tidy but there's certainly precedent. 😬 )

@pq
Copy link
Member Author

pq commented Jun 4, 2021

As for @jacob314's example, I do wonder if that might not be better addressed in the lint. That one feels like a false positive to me.

@dnfield
Copy link
Contributor

dnfield commented Jun 4, 2021

Another request that came up for this would be that if a Future returning function only contains @awaitNotRequired calls in it, it is also transitively @awaitNotRequired, e.g.

@awaitNotRequired
Future<void> doNotAwaitMe() async {
  print('ok');
}

Future<void> awaitMe() async {
  print('I sure hope you awaited this');
}

// Should be transitively @awaitNotRequired
Future<void> someFunction() async {
  await doNotAwaitMe(); // or don't await it, shouldn't matter.
  print('hi');
}

// Lint error to not await this function
Future<void> someOtherFunction() async {
  await awaitMe(); 
  print('hi');
}

@goderbauer
Copy link
Contributor

@dnfield Do we have an actual use case for that in the framework?

@bwilkerson
Copy link
Member

Another request that came up for this would be that if a Future returning function only contains @awaitNotRequired calls in it, it is also transitively @awaitNotRequired

I think that request means that such a function is treated as if it had been explicitly annotated without requiring an explicit annotation. If so, I'm not positive, but I suspect that that's something we can't efficiently implement.

(The only implementation I'm currently thinking of would be to create and incrementally maintain the call graph for all the code being analyzed, or at least for Future returning functions, in order to propagate requiredness through it. I think that doing so would be too inefficient to be practical.)

What we could do is add a lint to flag any such functions that aren't explicitly annotated as needing to be annotated, but such a lint might produce a lot of noise.

@dnfield
Copy link
Contributor

dnfield commented Jun 4, 2021

@Hixie was suggesting this. My impression is that we'd want to avoid needing to go up the chain of every function in the framework that calls annotated functions and avoiding awaiting them.

As I think about it, I wonder why that wouldn't be a good thing - we'd explicitly make choices about whether we really meant for a specific function to be awaited or not. But maybe @Hixie has some example I'm not thinking of.

@natebosch
Copy link
Member

Adding inference for this behavior would make it too fragile - local edits could have unpredictable consequences.

@lrhn
Copy link
Member

lrhn commented Jun 4, 2021

A @cannotThrow function should not depend on only calling other @cannotThrow functions. Sometimes you just know that it won't throw, even though it can, like calling int.parse with something you already know contains only digits.

Likewise, a function only calling @awaitNotRequired functions is not itself necessarily one. It could do something else between the calls that you should wait for, or decide that the not-required-wait future should actually be awaited.

I don't particularly want lint-related annotations in the platform libraries. A @pragma("analyzer:ignore_await_futures") annotation isn't out-of-scope. I just don't want to make it part of a public API.

Ignoring methods which happen to return a future due to generics, but which aren't inherently asynchronous seems somewhat reasonable, but also risky. It is a future, and it's impossible to know whether someone waited for it or not.
(Take: T id<T>(T value) => value; id(asyncOperation());. That future needs awaiting just as much as asyncOperation() itself. It's impossible to know whether a function propagates the future, or stores it somewhere - which is fine, an assignment isn't linted),
Heuristics can easily be wrong, general rules are at least predictable.

@fzyzcjy
Copy link
Contributor

fzyzcjy commented Mar 16, 2022

Hi, is there any updates :)

@pq pq mentioned this issue Jun 14, 2022
5 tasks
@srawlins
Copy link
Member

Hi @bsutton I can't tell from your link where the annotation would go or where the unawaited would go. It sort of looks like a user's code would not need any unawaited. Does the code in the future_builder_ex package just need unawaited? If @awaitNotRequired is not available?

@bsutton
Copy link

bsutton commented Mar 19, 2025

@srawlins

Here is a typical use case:

    body: FutureBuilderEx(
      // ignore: discarded_futures
      future: _signin(context),
      waitingBuilder:
          (context) => const Center(child: CircularProgressIndicator()),
      builder:
          (context, auth) => Center(

Note the need to ignore the discarded future.

In FutureBuilderEx I want to annotate the future param so that the discarded_futures lint is suppressed at all call sites.

class FutureBuilderEx<T> extends StatefulWidget {

const FutureBuilderEx({
    @unawaited /// suppress the discarded_futures lint wherever FutureBuilderEx is called.
    required this.future,
    required this.builder,
    super.key,
    this.initialData,
    this.waitingBuilder,
    this.errorBuilder,
    this.debugLabel = '',
  });

@bsutton
Copy link

bsutton commented Mar 19, 2025

I'm not certain that I like the 'unawaited' variants of the lint name as in my case it's not that the future is unawaited, it's just that it is handled/managed by the called function.

Maybe:

@managed_future

@owned_future

The parameter use case feels different to the examples that call a function that never want to be awaited.

@srawlins
Copy link
Member

// ignore: discarded_futures
      future: _signin(context),

Oh, this should be fixed by https://dart-review.googlesource.com/c/sdk/+/403901 - passing a Future as an argument that expects a Future is a valid 'use'.

@davidmorgan
Copy link
Contributor

@srawlins I sent a PR suggesting some doc changes for discarded_futures, but was mistaken about the details, it looks like your PR brings it closer to what I was expecting :) could you see what you think please? https://dart-review.googlesource.com/c/sdk/+/415441

@srawlins
Copy link
Member

Quick correction: "my PR" is written by @FMorschel

@davidmorgan
Copy link
Contributor

Oh, my mistake, thanks @FMorschel :)

@srawlins
Copy link
Member

OK latest question to resolve is the name. @lrhn , above, suggests @unawaited, using the same object available in dart:core. @lrhn says he is wary of recommending a lint that requires importing the meta package. I don't have any qualms about this.

Previously the discussion was @awaitNotRequired vs @doesNotThrow, and we went with @awaitNotRequired. I'd like folks to weigh in again re: unawaited. @natebosch @jakemac53 @davidmorgan @pq . @gspencergoog : who should I ping on the Flutter Framework team regarding this lint rule and annotation name?

@jakemac53
Copy link
Contributor

I personally am fine with recommending something that requires package:meta. In an ideal world the annotation would live in a core lib probably, but if we can't do that for whatever reason then we shouldn't allow that to block anything.

I don't think unawaited is a good fit for a declaration level annotation, because you can await the result, you just don't have to. I personally like @awaitNotRequired.

@gspencergoog
Copy link
Contributor

who should I ping on the Flutter Framework team regarding this lint rule and annotation name?

I'd talk to @justinmc or @Piinks about it.

@lrhn
Copy link
Member

lrhn commented Mar 24, 2025

The annotation also isn't necessary for clients of annotated libraries, so most people won't need @awaitNotRequired, only a few library writers.

About the name, if unawaited(...) can also be used with the discarded_futures lint, which applies to non-async functions that can't use await, then it's not just await that isn't required.
It's more "can safely be ignored". Still, awaiting a future is the common way to use it, so naming it after the most common use is probably fine.

@Piinks
Copy link
Contributor

Piinks commented Mar 25, 2025

Previously the discussion was @awaitNotRequired vs @doesNotThrow, and we went with @awaitNotRequired. I'd like folks to weigh in again re: unawaited. [...] : who should I ping on the Flutter Framework team regarding this lint rule and annotation name?

I like @awaitNotRequired over @unawaited. As I understand this, the option to await or not await is still up to the typer. The @awaitNotRequired seems more directive to me, while @unawaited sounds like it already might be handled for me.

@bsutton
Copy link

bsutton commented Mar 25, 2025 via email

@natebosch
Copy link
Member

+1 for @awaitNotRequired. I expect the annotation will be visible to users in the docs for APIs that use it and this name is the most clear for that audience.

@pq
Copy link
Member Author

pq commented Mar 27, 2025

+1 for @awaitNotRequired.

💯

+1 from me too.

@bwilkerson
Copy link
Member

Do we want to warn users if they're using the unawaited method on an expression covered by @awaitNotRequired? Seems like the call to unawaited is unnecessary at that point.

@srawlins
Copy link
Member

We could do that. An unnecessary_unawaited lint rule?

@bsutton
Copy link

bsutton commented Apr 23, 2025 via email

copybara-service bot pushed a commit that referenced this issue Apr 23, 2025
Work towards #46218

This implements the bulk of the `@awaitNotRequired` support as specified here: #46218 (comment)

Remaining work:

* implement inheritence
* report a badly-placed annotation
* add the annotation to the real meta package

Change-Id: Ida16018746704d0baa165ab9be95187b52a79061
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/423923
Reviewed-by: Konstantin Shcheglov <[email protected]>
Commit-Queue: Samuel Rawlins <[email protected]>
Reviewed-by: Brian Wilkerson <[email protected]>
@bwilkerson
Copy link
Member

I think it might be reasonable to make it a warning so that users don't need to enable it to get the benefit. The questions are:

  • Will there be false positives? I think the answer is 'no'.
  • How many users will want to disable it? I suspect the answer is 'almost none', though that's a little less certain in my mind.

@srawlins
Copy link
Member

Will there be false positives? I think the answer is 'no'.

There can be false positives when the function in question comes from a dependency, and the function is annotated for some releases of that dependency, and not annotated for others, in the accepted version constraint of the dependency.

@bwilkerson
Copy link
Member

Oh, yuck. I suppose we could have an since argument on the annotation so that we only generate the diagnostic if the minimum bound doesn't allow anything before that version, but that seems like an unnecessary affordance.

copybara-service bot pushed a commit that referenced this issue Apr 24, 2025
Work towards #46218

Change-Id: I9e356ab28996351d2ebfb15f655816f35a825339
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/424480
Reviewed-by: Brian Wilkerson <[email protected]>
Commit-Queue: Samuel Rawlins <[email protected]>
copybara-service bot pushed a commit that referenced this issue Apr 24, 2025
Work towards #46218

Change-Id: I41391cc317dec625f0b34afbf5f5a3402d8b08b4
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/424261
Reviewed-by: Konstantin Shcheglov <[email protected]>
Commit-Queue: Samuel Rawlins <[email protected]>
copybara-service bot pushed a commit that referenced this issue May 2, 2025
Work towards #46218

Change-Id: I0c6953463d053205cc1110f554578e6b85eac0db
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/426240
Commit-Queue: Samuel Rawlins <[email protected]>
Reviewed-by: Brian Wilkerson <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-devexp For issues related to the analysis server, IDE support, linter, `dart fix`, and diagnostic messages. devexp-pkg-meta Issues related to package:meta P3 A lower priority bug or feature request type-enhancement A request for a change that isn't a bug
Projects
None yet
Development

No branches or pull requests