1. Introduction
A promise 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 Promises/A+ community specification which most libraries conformed to. Now, a standard Promise
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.
This document previously defined a number of terms for manipulating promises, and gave examples for using them. Those have since moved to Web IDL. [WEBIDL]
Similarly, this document used to give advice on some of the general subtleties around asynchronous algorithms, i.e. running steps in parallel and queuing tasks. Those are now in HTML’s "Dealing with the event loop from other specifications" section.
2. When to use promises
2.1. One-and-done operations
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. undefined
), promises returned by asynchronous functions can be fulfilled with nothing (undefined
); 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 Appendix: legacy APIs for asynchronicity, 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.
2.2. One-time "events"
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 loaded
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 resource.loaded.then(onLoaded, onFailure)
. This will work even if the resource was loaded already, queueing a microtask to execute onLoaded
. 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.
2.3. More general state transitions
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 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 loaded
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 next 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. Streams uses this variant in several places, e.g. the 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 next 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 itssrc
. -
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.
3. When not to use promises
Although promises are widely applicable to asynchronous operations of many sorts, there are still situations where they are not appropriate, even for asynchronicity.
3.1. Recurring events
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.
3.2. Streaming data
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.
4. API design guidance
There are a few subtle aspects of using or accepting promises in your API. Here we attempt to address commonly-encountered questions and situations.
4.1. Errors
4.1.1. Promise-returning functions must always return promises
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
promiseFunction()
. then( onSuccess)
. catch ( onFailure);
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
block, and once in a p
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]
4.1.2. Rejection reasons must be Error
instances
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 built-in JavaScript error types, or using DOMException
.
4.1.3. Rejections must be used for exceptional situations
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 null
, false
, or undefined
)? 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
accessUsersContacts()
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
asyncMap.get("key")
should return a promise forundefined
when there is no entry for"key"
, and similarlyasyncMap.has("key")
should return a promise forfalse
. The absence of"key"
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
hasPermissionToAccessUsersContacts()
and the user has denied permission, then it should return a promise fulfilled withfalse
; 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
requestUsersContacts()
could return a promise fulfilled withnull
if the user denies permission, or it could return a promise rejected with an error stating that the user denied permission.
4.2. Accepting promises
4.2.1. Promise arguments should be resolved
In general, when an argument is expected to be a promise, you should also allow thenables and non-promise values by resolving the argument to a promise before using it. You should never 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 Promise<T>
type.
To see what it means in JavaScript code, consider the following function, which adds a delay of ms milliseconds to a promise:
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 );
In this example, p1
will be fulfilled 500 ms after the promise returned by doAsyncOperation()
fulfills, with that operation’s value. (Or p1
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 "value"
: p2
will be fulfilled with "value"
after 1000 ms. In this way, we essentially treat it as an immediately-fulfilled promise for that value.
4.2.2. Developer-supplied promise-returning functions should be "promise-called"
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 Promise.resolve()
, and catching thrown exceptions and converting those into a promise as if by using Promise.reject()
. 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.
Appendix: legacy APIs for asynchronicity
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:
-
IndexedDB returning
IDBRequest
objects, with theironsuccess
andonerror
event handler attributes. [INDEXEDDB] -
File API: Directories and System’s methods taking various
successCallback
anderrorCallback
parameters. [FILE-SYSTEM-API] -
Notifications’s
requestPermission(deprecatedCallback)
operation, which calls its callback with"granted"
or"denied"
. (This has since been updated to also return a promise, making the callback optional.) [NOTIFICATIONS] -
XMLHttpRequest’s
send()
method, which triggersonreadystatechange
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.