Skip to content
This repository was archived by the owner on Oct 7, 2020. It is now read-only.

user-expectations-6 #21

Merged
merged 2 commits into from
May 26, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion meeting-discussion.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ How to read this:
- We don't have documentation of how the promise queue is integrated into Node.js (use case #expectations-3)
- Users expect to be able to use `async/await` with things like `.forEach` but they can't - can we communicate it better? (with warnings for no returned promises). (use case #expectations-4)
- Some people transpiling to JavaScript from other languages have reported a hard time with meaningful stack traces. Reference: https://github.com/degree9/meta/issues/22
- Extracting resolution/rejection abilities from Promises is awkward and confusing. (use case #exepctations-5)
- Extracting resolution/rejection abilities from Promises is awkward and confusing. (use case #expectations-5)
- Synchronous exception in Promise constructor always rejects even if promise is already resolved. (use case #expectations-6)

### Extras:
- Can't implement userland features on top of native promises - such as cancellation, because of `async`/`await` (#extras-1)
Expand Down
76 changes: 76 additions & 0 deletions use-cases/user-expectations/6/expectations-6.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
### Use Case #6

Name: Synchronous exception in Promise constructor always rejects even if promise is already resolved.

Brandy is writing code with Promises. She writes the following program, and is surprised/confused by not seeing the `"resolved!"` message in the console. That confusion causes her to worry that the Promise constructor is not even running, even though it seems like `showSuccess()` is being called.

```js
var OK = true;

var p = new Promise(function c(resolve,reject){
if (OK) {
resolve("Good to go!");
cosnole.log("resolved!"); // console misspelled here
}
else {
reject("Oops!");
console.log("rejected.");
}
});

p.then( showSuccess, showError );
// ..nothing printed to console :(
```

### What happens

The promise is already fulfilled (with `resolve(..)`), so the exception that then happens from `cosnole.log(..)` being misspelled is silently swallowed.

### Why it happens

Promises can only be resolved to a fulfillment or rejection, and once resolved, cannot be changed. This normally makes sense, but in this specific case of an accidental exception that occurs synchronously subsequent to the otherwise resolution (fulfillment or rejection), the user never expects the promise machinery to just suppress/hide that exception.

### What can we maybe do better
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be interesting to also suggest an asnyc_hook here - a function that will be called on all throws inside a promise constructor and report them to APMs or implement the behavior below, WDYT?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I may not fully know what an "async_hook" is, but I shyed away from that because this use-case is specifically about synchronous behavior (both the resolution and the exception overriding it).

Having a hook to be notified of these cases is not a bad idea, but I organized this use-case under "expectations" because I think most users would tend to just expect this suggested behavior. So a hook would sorta be augmentative of that rather than an alternative.


This could be seen as a case for a warning. But that warning would likely be confusing to track down, because it's likely going to talk about the promise trying to be rejected after it's already resolved. This code doesn't look like it matches that pattern, since there's a clear `if-else` that either fulfills or rejects. It may be confusing for the user to connect-the-dots that the excess rejection is actually an unexpected JS exception.

In this case, a synchronous exception is different from if the user had mistakenly called `reject(..)`. It's not a mistake of the usage of the API, but (arguably) a more serious exception of writing broken JS. Being more serious, it calls for this case to be handled differently from handling for simply using the Promise mechanism incorrectly.

By virtue of how the promise is constructed (with a synchronous resolution and `if..else`), the user is expecting either the fulfillment or rejection paths to cover all possible outcomes. The user is most likely any exception to result in the promise having been rejected, with the exception as its reason. As such, the most appropriate place to report the exception is in the promise rejection path. Thus, the synchronous exception needs to override the prior synchronous resolution.

Because the suggested change would be breaking backwards compatibility with current promises, the user should be able to opt-in to the different behavior **prior to the construction**. For example, they could set a flag to turn on a sort of "strict mode" for the Promise construction.

----

Additional Notes:

1. The suggested behavior for the constructor is implemented in this [`BetterPromise` experiment](https://gist.github.com/getify/1173cac45d15fc4ff0a880f32fd598ab).

2. This notion of suggested overriding is not without precedent in JS, btw. The `finally {..}` clause in a function is notably able to override a previous synchronous `return` value from that same function.

In fact, one could think of this suggested behavior kind of like sugaring for this:

```js
var p = new Promise(function c(resolve,reject){
var _fulfillment;
var _rejection;

try {
if (OK) {
_fulfillment = "Good to go!";
cosnole.log("resolved!"); // console misspelled here
}
else {
_rejection = "Oops!";
console.log("rejected.");
}
}
catch (err) {
_rejection = err;
}
finally {
if (_rejection) reject(_rejection);
else if (_fulfillment) resolve(_fulfillment);
}
});
```