Skip to content

Promise rejection type. #39680

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

Open
5 tasks done
RebeccaStevens opened this issue Jul 21, 2020 · 17 comments
Open
5 tasks done

Promise rejection type. #39680

RebeccaStevens opened this issue Jul 21, 2020 · 17 comments
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@RebeccaStevens
Copy link

Search Terms

Promise Reject Type

Suggestion

Add ability to type Promise rejections.

// Current Promise Constructor Implementation:
new <T>(executor: (resolve: (value?: T | PromiseLike<T>) => void, reject: (reason?: any) => void) => void): Promise<T>;

// Proposed Change:
new <T, E = any>(executor: (resolve: (value?: T | PromiseLike<T>) => void, reject: (reason?: E) => void) => void): Promise<T, E>;

Use Cases

When handling promise rejections, the type any isn't very useful. I would be useful to have the rejection have an actual type without the need to cast or type guard.

Examples

Promise.reject<never, string>('hello world')
  .catch(reason => {
    console.error(reason.length); // `reason` is of type string.
  });
class MyError extends Error {
  // ...
}

Promise.reject<never, MyError>(new MyError(/* ... */))
  .catch(reason => {
    // `reason` is of type MyError.
    const info = reason.getMoreInfo();
    // ...
  });

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • 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.
@IllusionMH
Copy link
Contributor

Looks like duplicate of #6283 and other similar.
See for more details #6283 (comment)

@RyanCavanaugh RyanCavanaugh added Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript labels Jul 21, 2020
@RebeccaStevens
Copy link
Author

RebeccaStevens commented Jul 28, 2020

I don't feel like they rejection type always has to be of type any/unknown.

It is obviously possible to detect what the rejection type is when Promise.reject is used. The hard bit would be detecting rejections caused by a throw statement inside an async function. But if we approach it from the perspective of a throw statement just being an alternative type of return statement, it seems possible to me (but maybe quite difficult).

I've been having a bit of a play with this to see what it would be like add support. This is what I've got so far just by playing with type definitions.

@IllusionMH
Copy link
Contributor

p2                                         // p2 is of type Promise<number, string>
  .then((v2) =>  void cosnole.log(v2))     // v2 is of type number
  .catch((e2) => void console.error(e2));  // e2 is actually type string | ReferenceError, not string

@RebeccaStevens
Copy link
Author

Why would the type be string | ReferenceError? Isn't TypeScript already be reporting ReferenceError when they happen (2304)? (Or is that a strict mode only thing? If so then this proposal could just be for strict mode.)

playground

@IllusionMH
Copy link
Contributor

IllusionMH commented Jul 29, 2020

Yes, TS will report 2304 in this obvious case however emitted JS code will still have a change to receive both string and ReferenceError. Idea is to show that if there is something between resolve and catch - there are no guarantees that code between them wont throw something else (e.g. failed network request, incorrect normalize function etc.)

Looks like string will be only true for case p2.catch(...) but only if you believe that there are no other errors before resolve/reject (which might not be always true).

@RebeccaStevens
Copy link
Author

Could you give a small example?

If myPromise.then(...).catch(...) can't be handled due to this but myPromise.catch(...) could be, imo that's still worth pursuing.

@IllusionMH
Copy link
Contributor

In unsound cases or unknown external code (e.g. computedBoolean) there is always a chance to have behavior that will throw

declare const computedBoolean: (params: any) => boolean; // but in general case might throw
const list = [{id: 111}, {id: 222}];

function firstThreeIds(data: Array<{id: number}>): number[] {
    return [
      data[0].id,
      data[1].id,
      data[2].id // no compile time error, but will throw
    ];
}

const p = new Promise<number[], string>((resolve, reject) => {
  if (computedBoolean(list)) { // can throw before resolve
    resolve(firstThreeIds(list)); // wil throw before resolve
  } else {
    reject("foo");
  }
});


p
  .catch((e) => { /* ... */}); // actually string | TypeError

So it looks like closest thing e can be typed is E | Error in definition (string | Error in current example).
And again if computedBoolean won't throw anything else (not sure which case will be encountered more often).

@RebeccaStevens
Copy link
Author

RebeccaStevens commented Jul 31, 2020

Could this proposal solve this issue?
#13219

@mateja176 mateja176 mentioned this issue Oct 10, 2020
5 tasks
remss pushed a commit to remss/ws-await that referenced this issue May 11, 2021
Promise rejection errors as jsdoc, because it is not supported in TS yet (microsoft/TypeScript#39680).
Use Generics to type resolved value and message event data.
remss added a commit to remss/ws-await that referenced this issue May 19, 2021
Promise rejection errors as jsdoc, because it is not supported in TS yet (microsoft/TypeScript#39680).
Use Generics to type resolved value and message event data.
@thw0rted
Copy link

I just opened #45869. I think maybe the best-case outcome would be implementing #13219 (throws-clause) and this issue, in which case it would become possible to document Promises that have a rejects type of never, i.e. rejection is not possible. (Think return Promise.resolve(1);, etc.)

It would be a lot of work for most projects to migrate to throws-clause compliance but once done, the type checker would be able to reason about exception / rejection paths -- I would love to automate that reasoning, rather than getting bitten by orphaned async-function calls and having to hunt them down to fix on my own.

@Bec-k
Copy link

Bec-k commented Nov 28, 2022

I'm confused, is this implemented or not? Can't find any info in google.

@RebeccaStevens
Copy link
Author

It's not. The rejection type is always unknown

@steverep
Copy link

steverep commented Aug 8, 2023

Just encountered working with reason as an any type. I understand why it doesn't have a specific type, but I guess I'm wondering why it's not instead typed as unknown? There's a similar situation for the error property of ErrorEvent as it's completely non-standard.

In both situations, it seems that the appropriate type should be unknown, which by definition forces the developer into safe type guards. Having it typed as any just takes TS out of the picture altogether.

@kevinbarabash
Copy link

Since this change isn't breaking you could always monkey patch the appropriate .d.ts file with the proposed change. I think it would be nice if this made it into the official typings. There still other changes that would need to be made to the compiler to get a good developer experience. For instance, if you throw inside of a then() block, it would be nice if TS could infer that update the rejection type appropriately. There was a ticket about adding support for tracking the exception types thrown by functions, but it was closed earlier this year, see #13219 (comment). I'm guessing this proposal is dead-in-the-water for similar reasons.

@M-jerez
Copy link

M-jerez commented Aug 14, 2023

I don't understand the argument that the rejection type can't be guaranteed.
Nothing can be guaranteed by typescript at runtime. Don't see why rejection type is different.
If I'm telling typescript rejection type is a certain type is because I'm supposed to know what I'm doing. Typescript code is usually full of castings or assuming some stuff is defined, etc.. Don't see why this should be any different.

And yes maybe in the try-catch might be always any, but at least it should be typed when using Promise.catch.

@thw0rted
Copy link

If you read the issues I linked from my previous post (2 years ago!), you'll see some of the reasoning. I think one of the most compelling arguments against allowing manual declaration of a throws or rejects type is that a novice who reads your code is unlikely to realize that you're making an unsafe assertion, along the lines of a typecast.

I'd still really like to see a way to annotate Promises that cannot reject, though. I actually write a lot of async functions with top level try/catch - it's a good pattern to follow when implementing an Express router callback, since the server library will not handle a rejection for you.

@M-jerez
Copy link

M-jerez commented Aug 14, 2023

I think one of the most compelling arguments against allowing manual declaration of a throws or rejects type is that a novice who reads your code is unlikely to realize that you're making an unsafe assertion, along the lines of a typecast.

In this case is the developer that set the return type the one that needs to guarantee the thrown exception is the correct type. I'm telling typescript I will only reject with that kind of type.

In typescript is perfectly valid to write

function sum(a: number, b: number): number {
  return 'hello world' as any as number;
}

This is totally valid TS although would fail at runtime, don't see why the developer could not explicitly tells typescript he will make sure the rejected value is a certain type. I'm not asking typescript to check thrown types etc. just so the consumers of the promises know the rejected type.

@thw0rted
Copy link

Sorry I wasn't more specific, but I wrote the previous comment from a phone. I was referring to #45869 (comment) :

it’s really not obvious that there is an assertion. Experienced TS developers are attuned to look out for unsoundness when they see as type assertions; this proposal makes the unsoundness very easy to overlook.

The point was that, if rejection-types were implemented, I could write

async function lookup(key: string): Promise<string, never> {
  try {
    return maybeGetValueEventually(key);
  } catch {
    logThatMightThrow(key);
    return "fallback";
  }
}

but without comprehensive checked exceptions (a feature that has already been rejected) the type-checker couldn't prove that the function never rejects.

In this example, the first generic argument in the returned-value Promise type (string) is type-checked -- if I wrote return 0 in the catch block instead of return "fallback", it would get flagged as an error -- but the second generic argument (never) is an unsafe assertion that I'm making. There's no way for the typechecker to determine if the Promise never actually rejects, but there's no cast or as keyword to act as a red flag. You'd need a pretty deep expertise in TypeScript to actually understand that difference.

I do think this could be handled by a linter rule ("all explicit Promise rejection types require a comment"), and I would still argue that some construct for asserting at its source that a Promise cannot reject is useful enough to merit a certain amount of novice-developer hazard, but I wanted to point out that there is a totally reasonable counterargument here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

8 participants