Skip to content
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

Strict await #35419

Closed
5 tasks done
Yogu opened this issue Nov 29, 2019 · 11 comments
Closed
5 tasks done

Strict await #35419

Yogu opened this issue Nov 29, 2019 · 11 comments
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@Yogu
Copy link
Contributor

Yogu commented Nov 29, 2019

Search Terms

errors on await on non-promise values, await expecting then()

Suggestion

I'd like an option (e.g. strictAwait) that causes await on non-promise expressions to be an error.

Awaiting values of union types where one option is a promise probably should still be ok, so you can await on something that could be a promise or a value.

The following conditional type would reflect this behavior AFAIK:

type MayBePromise<T> = T extends PromiseLike<any> ? true : never;

await would then only be allowed on values of type T where MayBePromise<T> is equal to true.

Use Cases

While it's not a direct runtime error to await a non-promise value, it's still most likely a bug, because you expected something to be a promise, but it isn't.

The most common case for me is a missing call to toPromise() on an observable I just want to activate without caring about the result. This is an example using apollo-client, where the mutate method returns a cold observable that you need to subscribe to in order to actually do something:

class Service {
  constructor(private apollo: Apollo) {}

  async doSomething() {
    await this.apollo.mutate({ /* ... */ }).toPromise();
  }
}

If you omit the toPromise(), everything still typechecks, but nothing will happen.

Calling await on immediate values won't do any harm (apart from performance probably), but it's still misleading, and it will be a hard-to-find bug when a then method will be added to the value in the future.

Examples

This should still compile:

const promiseA: Promise<string> = undefined as any;
const a: string = await promiseA;

const promiseB: Promise<string>| string = undefined as any;
const a: string = await promiseA;

// result type of  `await` does not change, it still merges promise and non-promise types
const promiseC: Promise<string>| number = undefined as any;
const C: string | number = await promiseA;

This should not compile:

const nonPromise: number = undefined as any;
const err: number = await nonPromise;

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code (It'd be a new option, disabled by default)
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.
@MartinJohns
Copy link
Contributor

MartinJohns commented Nov 29, 2019

It's perfectly valid in JavaScript to await on a non-promise expression:

If the value of the expression following the await operator is not a Promise, it's converted to a resolved Promise.

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await

If you really want to prevent this, then a linter seems much more appropriate.

@Yogu
Copy link
Contributor Author

Yogu commented Nov 29, 2019

It's perfectly valid in JavaScript to await on a non-promise expression:

I know, that's why I explained my reasining in the use case section. I think this is similar to checking the thruthyness of a function value - that's also perfectly valid, but most likely a bug. I'm glad TypeScript now reports this an an error.

@kzok
Copy link

kzok commented Nov 29, 2019

@typescript-eslint has the rule await-thenable that disallows awaiting non-promise value.

@Yogu
Copy link
Contributor Author

Yogu commented Dec 2, 2019

@kzok Thanks, didn't know this existed. Indeed, a linter seems to be appropiate for this.

@Yogu Yogu closed this as completed Dec 2, 2019
@RyanCavanaugh RyanCavanaugh added the Working as Intended The behavior described is the intended behavior; this is not a bug label Dec 2, 2019
@RyanCavanaugh
Copy link
Member

This is intentional; await x when x is e.g. string | Promise<string> is a good ™ idiom. await on a non-Promise may also be used intentionally to allow an event loop tick to proceed.

@MLoughry
Copy link

@RyanCavanaugh, could this strict mode simply not error on the former case where a type is possibly a Promise (eg., string | Promise<string>)?

As for the latter case, could it not be handled by either leaving the mode disabled, or using // @ts-ignore to disable the specific error?

@MartinJohns
Copy link
Contributor

Using // @ts-ignore should not be the way to deal with valid type-safe code. It's not an error.

This seems to be a job for a linter, but not for the compiler.

@MLoughry
Copy link

This seems to be a job for a linter, but not for the compiler.

Counterpoint: Couldn't the same be said for uncalled function checks?

To lay bare my personal motivations, we currently use the await-promise TSLint rule for this exact case. However, it is the only rule we use that makes use of type information, and our >1M lines-of-code project takes several minutes to initialize a TS program for TSLint. If this were folded into Typescript, then we won't need to initialize a TS program twice.

@peterflynn
Copy link

@RyanCavanaugh is there documentation with guidance on where this line is drawn – what kinds of issues the compiler would be willing to treat as warnings vs. these lighter "advisories"? It seems like there are some examples where code that's technically valid can be treated as a compilation warning or error (e.g. uncalled / always-truthy methods in if statements, or the noUnusedLocals & noUnusedParameters options).

@jpike88
Copy link

jpike88 commented Aug 31, 2023

await on a non-Promise may also be used intentionally to allow an event loop tick to proceed.

That sounds like some low-level weirdness that shouldn't be encouraged, and opted-in somehow.

@ondravondra
Copy link

ondravondra commented Jul 19, 2024

Another letdown by TS. Consider this code:

function callMyApi() {
  api.somecall(); // forgot return -> return type of callMyApi is therefore void instead of Promise
}

async function complexOperation() {
  await something();
  await callMyApi();
  // here we expect the api call to be successfully completed
  await somethingElse();
  // await does not wait and a race condition happens
}

This code should not compile. But at least there's an eslint rule.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

8 participants