-
Notifications
You must be signed in to change notification settings - Fork 24
/
index.bs
187 lines (116 loc) · 18.2 KB
/
index.bs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
<pre class="metadata">
Title: Writing Promise-Using Specifications
Group: tag
Shortname: promises-guide
Status: DRAFT-FINDING
TR: https://www.w3.org/2001/tag/doc/promises-guide
ED: https://w3ctag.github.io/promises-guide/
Editor: Domenic Denicola, Google https://www.google.com/, https://domenic.me/, [email protected]
Abstract: This document gives guidance on how to write specifications that create, accept, or manipulate promises.
Boilerplate: omit conformance
Default Biblio Status: current
!Participate: <a href="https://github.com/w3ctag/promises-guide">GitHub w3ctag/promises-guide</a> (<a href="https://github.com/w3ctag/promises-guide/issues/new">file an issue</a>; <a href="https://github.com/w3ctag/promises-guide/issues?state=open">open issues</a>)
</pre>
<pre class="anchors">
urlPrefix: https://tc39.es/ecma262/#; spec: ECMASCRIPT
text: Error; url: sec-error-objects; type: interface
urlPrefix: https://notifications.spec.whatwg.org/#; spec: NOTIFICATIONS
text: requestPermission(deprecatedCallback); url: dom-notification-requestpermission; type: method; for: Notification
</pre>
<pre class="link-defaults">
spec:streams; type:interface; text:ReadableStream
spec:webidl; type:dfn; text:resolve
</pre>
<h2 id="intro">Introduction</h2>
A <em>promise</em> is an object that represents the eventual result of a single asynchronous operation. They can be returned from asynchronous functions, thus allowing consumers to not only queue up callbacks to be called when the operation succeeds or fails, but also to manipulate the returned promise object, opening up a variety of possibilities.
Promises have been battle-tested in many JavaScript libraries, including as part of popular frameworks like Dojo, jQuery, YUI, Ember, Angular, WinJS, Q, and others. This culminated in the <a href="https://promisesaplus.com/">Promises/A+ community specification</a> which most libraries conformed to. Now, a standard <a href="https://tc39.github.io/ecma262/#sec-promise-objects"><code>Promise</code></a> class is included in the JavaScript specification, allowing web platform APIs to return promises for their asynchronous operations. [[!ECMASCRIPT]]
Promises are now the web platform's paradigm for all "one and done" asynchronous operations. Previously, specifications used a variety of mismatched mechanisms for such operations. Going forward, all asynchronous operations of this type should be specified to instead return promises, giving our platform a unified primitive for asynchronicity.
<p class="note" oldids="shorthand-phrases a-new-promise a-promise-resolved-with resolved-as-a-promise a-promise-rejected-with resolve-promise reject-promise upon-fulfillment upon-rejection transforming-by waiting-for-all waiting-for-all-promise promise-calling examples example-delay example-validated-delay example-add-delay example-resource-open example-environment-ready example-add-bookmark example-batch-request shorthand-note-on-realms webidl-examples webidl">This document previously defined a number of terms for manipulating promises, and gave examples for using them. Those have since moved to <cite>Web IDL</cite>. [[!WEBIDL]]</p>
<p class="note" oldids="async-algorithms explicit-async-steps queue-tasks">Similarly, this document used to give advice on some of the general subtleties around asynchronous algorithms, i.e. running steps [=in parallel=] and [=queue a task|queuing tasks=]. Those are now in <cite>HTML</cite>'s "<a href="https://html.spec.whatwg.org/multipage/webappapis.html#event-loop-for-spec-authors">Dealing with the event loop from other specifications</a>" section.</p>
<h2 id="when-to-use">When to use promises</h2>
<h3 id="one-and-done">One-and-done operations</h3>
The primary use case for promises is returning them from a method that kicks off a single asynchronous operation. One should think of promise-returning functions as asynchronous functions, in contrast to normal synchronous functions; there is a very strong analogy here, and keeping it in mind makes such functions easier to write and reason about.
For example, normal synchronous functions can either return a value or throw an exception. Asynchronous functions will, analogously, return a promise, which can either be fulfilled with a value, or rejected with a reason. Just like a synchronous function that returns "nothing" (i.e. <code>undefined</code>), promises returned by asynchronous functions can be fulfilled with nothing (<code>undefined</code>); in this case the promise fulfillment simply signals completion of the asynchronous operation.
Examples of such asynchronous operations abound throughout web specifications:
- Asynchronous I/O operations: methods to read or write from a storage API could return a promise.
- Asynchronous network operations: methods to send or receive data over the network could return a promise.
- Long-running computations: methods that take a while to compute something could do the work on another thread, returning a promise for the result.
- User interface prompts: methods that ask the user for an answer could return a promise.
Previously, web specifications used a large variety of differing patterns for asynchronous operations. We've documented these in [[#legacy]], so you can get an idea of what to avoid. Now that we have promises as a platform primitive, such approaches are no longer necessary.
<h3 id="one-time-events">One-time "events"</h3>
Because promises can be subscribed to even after they've already been fulfilled or rejected, they can be very useful for a certain class of "event." When something only happens once, and authors often want to observe the status of it after it's already occurred, providing a promise that becomes fulfilled when that eventuality comes to pass gives a very convenient API.
The prototypical example of such an "event" is a loaded indicator: a resource such as an image, font, or even document, could provide a <code>loaded</code> property that is a promise that becomes fulfilled only when the resource has fully loaded (or becomes rejected if there's an error loading the resource). Then, authors can always queue up actions to be executed once the resource is ready by doing <code>resource.loaded.then(onLoaded, onFailure)</code>. This will work even if the resource was loaded already, [=queue a microtask|queueing a microtask=] to execute <code>onLoaded</code>. This is in contrast to a traditional event model, such as that of {{EventTarget}}, where if the author is not subscribed at the time the event fires, that information is lost.
<h3 id="state-transitions">More general state transitions</h3>
In certain cases, promises can be useful as a general mechanism for signaling state transitions. This usage is subtle, but can provide a very nice API for consumers when done correctly.
One can think of this pattern as a generalization of the one-time "events" use case. For example, take <{img}> elements. By resetting their <{img/src}> attribute, they can be re-loaded; that is, they can transition back from a loaded state to an unloaded state. Thus becoming loaded is not a one-time occasion: instead, the image actually consists of a state machine that moves back and forth between loaded and unloaded states.
In such a scenario, it is still useful to give images a promise-returning <code>loaded</code> property, which will signal the next state transition to a loaded state (or be already fulfilled if the image is already in a loaded state). This property should return the same promise every time it is retrieved, until the image moves backward from the loaded state into the unloaded state. Once that occurs, a new promise is created, representing the <em>next</em> transition to loaded.
There are many places in the platform where this can be useful, not only for resources which can transition to loaded, but e.g. for animations that can transition to finished, or expensive resources that can transition to disposed, or caches that can become invalidated.
A slight variant of this pattern occurs when your class contains a method that causes a state transition, and you want to indicate when that state transition completes. In that case you can return a promise from the method, instead of keeping it as a property on your object. <cite>Streams</cite> uses this variant in several places, e.g. the {{WritableStreamDefaultWriter/close()|writer.close()}} method. In general, methods should be used for actions, and properties for informational state transitions.
To close, we must caution against over-using this pattern. Not every state transition needs a corresponding promise-property. Indicators that it might be useful include:
- Authors are almost always interested in the <em>next</em> instance of that state transition, and rarely need recurring notification every time it occurs. For example, rarely do authors care to know every time an <{img}> is reloaded; usually they simply care about the initial load of the image, or possibly the next one that occurs after resetting its <{img/src}>.
- Authors are often interested in reacting to transitions that have already occurred. For example, authors often want to run some code once an image is loaded; if the image is already loaded, they want to run the code as soon as possible.
<h2 id="when-not-to-use">When not to use promises</h2>
Although promises are widely applicable to asynchronous operations of many sorts, there are still situations where they are not appropriate, even for asynchronicity.
<h3 id="recurring-events">Recurring events</h3>
Any event that can occur more than once is not a good candidate for the "one and done" model of promises. There is no single asynchronous operation for the promise to represent, but instead a series of events. Conventional {{EventTarget}} usage is just fine here.
<h3 id="streaming-data">Streaming data</h3>
If the amount of data involved is potentially large, and could be produced incrementally, promises are probably not the right solution. Instead, you'll want to use the {{ReadableStream}} class, which allows authors to process and compose data streams incrementally, without buffering the entire contents of the stream into memory.
Note that in some cases, you could provide a promise API alongside a streaming API, as a convenience for those cases when buffering all the data into memory is not a concern. But this would be a supporting, not primary, role.
<h2 id="api-design-guidance">API design guidance</h2>
There are a few subtle aspects of using or accepting promises in your API. Here we attempt to address commonly-encountered questions and situations.
<h3 id="errors">Errors</h3>
<h4 id="always-return-promises" oldids="webidl-promise-return-values">Promise-returning functions must always return promises</h4>
Promise-returning functions must always return a promise, under all circumstances. Even if the result is available synchronously, or the inputs can be detected as invalid synchronously, this information needs to be communicated through a uniform channel so that a developer can be sure that by doing
<pre><code class="lang-javascript">
promiseFunction()
.then(onSuccess)
.catch(onFailure);
</code></pre>
they are handling all successes and all errors.
In particular, promise-returning functions should never synchronously throw errors, since that would force duplicate error-handling logic on the consumer: once in a <code class="lang-javascript">catch (e) { ... }</code> block, and once in a <code class="lang-javascript">p.catch(e => { ... })</code> block. Even argument validation errors are not OK. Instead, all errors should be signaled by returning rejected promises.
For Web IDL-based specs, this is taken care of automatically if you declare your [=operations=] to return a [=promise type=]. Any exceptions thrown by such operations, or by the Web IDL-level type conversions and overload resolution, are automatically converted into rejections. [[!WEBIDL]]
<h4 id="reasons-should-be-errors">Rejection reasons must be {{Error}} instances</h4>
Promise rejection reasons should always be instances of the JavaScript {{Error}} type, just like synchronously-thrown exceptions should always be instances of {{Error}}. This generally means using either one of the <a href="https://tc39.github.io/ecma262/#sec-error-objects">built-in JavaScript error types</a>, or using {{DOMException}}.
<h4 id="rejections-should-be-exceptional">Rejections must be used for exceptional situations</h4>
What exactly you consider "exceptional" is up for debate, as always. But, you should always ask, before rejecting a promise: if this function was synchronous, would I expect a thrown exception under this circumstance? Or perhaps a failure value (like <code>null</code>, <code>false</code>, or <code>undefined</code>)? You should think about which behavior is more useful for consumers of your API. If you're not sure, pretend your API is synchronous and then think if your developers would expect a thrown exception.
Good cases for rejections include:
- A failed I/O operation, like writing to storage or reading from the network.
- When it will be impossible to complete the requested task: for example if the operation is <code>accessUsersContacts()</code> and the user denies permission, then it should return a rejected promise.
- Any situation where something is internally broken while attempting an asynchronous operation: for example if the developer passes in invalid data, or the environment is in an invalid state for this operation.
Bad uses of rejections include:
- When a value is asked for asynchronously and is not found: for example <code>asyncMap.get("key")</code> should return a promise for <code>undefined</code> when there is no entry for <code>"key"</code>, and similarly <code>asyncMap.has("key")</code> should return a promise for <code>false</code>. The absence of <code>"key"</code> would be unexceptional, and so a rejected promise would be a poor choice.
- When the operation is phrased as a question, and the answer is negative: for example if the operation is <code>hasPermissionToAccessUsersContacts()</code> and the user has denied permission, then it should return a promise fulfilled with <code>false</code>; it should not reject.
Cases where a judgement call will be necessary include:
- APIs that are more ambiguous about being a question versus a demand: for example <code>requestUsersContacts()</code> could return a promise fulfilled with <code>null</code> if the user denies permission, or it could return a promise rejected with an error stating that the user denied permission.
<h3 id="accepting-promises">Accepting promises</h3>
<h4 id="resolve-arguments" oldids="webidl-promise-parameters">Promise arguments should be resolved</h4>
In general, when an argument is expected to be a promise, you should also allow thenables and non-promise values by <em>resolving</em> the argument to a promise before using it. You should <em>never</em> do a type-detection on the incoming value, or overload between promises and other values, or put promises in a union type.
In Web IDL-using specs, this is automatically taken care of by the <code><a interface>Promise</a><<var ignore>T</var>></code> type.
To see what it means in JavaScript code, consider the following function, which adds a delay of <var ignore>ms</var> milliseconds to a promise:
<div class="example">
<pre><code class="lang-javascript">
function addDelay(promise, ms) {
return Promise.resolve(promise).then(v =>
new Promise(resolve =>
setTimeout(() => resolve(v), ms);
)
);
}
var p1 = addDelay(doAsyncOperation(), 500);
var p2 = addDelay("value", 1000);
</code></pre>
</div>
In this example, <code>p1</code> will be fulfilled 500 ms after the promise returned by <code>doAsyncOperation()</code> fulfills, with that operation's value. (Or <code>p1</code> will reject as soon as that promise rejects.) And, since we resolve the incoming argument to a promise, the function can also work when you pass it the string <code>"value"</code>: <code>p2</code> will be fulfilled with <code>"value"</code> after 1000 ms. In this way, we essentially treat it as an immediately-fulfilled promise for that value.
<h4 id="should-promise-call" oldids="webidl-developer-functions-returning-promises">Developer-supplied promise-returning functions should be "promise-called"</h4>
If the developer supplies you with a function that you expect to return a promise, you should also allow it to return a thenable or non-promise value, or even throw an exception, and treat all these cases as if they had returned an analogous promise. This should be done by converting the returned value to a promise, as if by using <code>Promise.resolve()</code>, and catching thrown exceptions and converting those into a promise as if by using <code>Promise.reject()</code>. We call this "promise-calling" the function.
The purpose of this is to allow us to have the same reaction to synchronous forms of success and failure that we would to asynchronous forms.
In Web IDL-using specifications, this is automatically taken care of if you declare the developer function as a [=callback function=] and then [=invoke=] it.
<h2 id="legacy" class="no-num">Appendix: legacy APIs for asynchronicity</h2>
<div class="non-normative">
Many web platform APIs were written before the advent of promises, and thus came up with their own ad-hoc ways of signaling asynchronous operation completion or failure. These include:
- <cite>IndexedDB</cite> returning {{IDBRequest}} objects, with their {{IDBRequest/onsuccess}} and {{IDBRequest/onerror}} event handler attributes. [[INDEXEDDB]]
- <cite>File API: Directories and System</cite>'s methods taking various <code>successCallback</code> and <code>errorCallback</code> parameters. [[FILE-SYSTEM-API]]
- <cite>Notifications</cite>'s {{Notification/requestPermission(deprecatedCallback)}} operation, which calls its callback with <code>"granted"</code> or <code>"denied"</code>. (This has since been updated to also return a promise, making the callback optional.) [[NOTIFICATIONS]]
- <cite>XMLHttpRequest</cite>'s {{XMLHttpRequest/send()}} method, which triggers {{XMLHttpRequest/onreadystatechange}} multiple times and updates properties of the object with status information which must be consulted in order to accurately detect success or failure of the ultimate state transition. [[XHR]]
If you find yourself doing something even remotely similar to these, stop, and instead use promises.
</div>