Writing Promise-Using Specifications

Finding of the W3C TAG,

Editor:
Domenic Denicola (Google)
Participate:
GitHub w3ctag/promises-guide (file an issue; open issues)

Abstract

This document gives guidance on how to write specifications that create, accept, or manipulate promises.

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:

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:

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 catch (e) { ... } block, and once in a p.catch(e => { ... }) 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:

Bad uses of rejections include:

Cases where a judgement call will be necessary include:

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:

If you find yourself doing something even remotely similar to these, stop, and instead use promises.

Index

Terms defined by reference

References

Normative References

[DOM]
Anne van Kesteren. DOM Standard. Living Standard. URL: https://dom.spec.whatwg.org/
[ECMASCRIPT]
ECMAScript Language Specification. URL: https://tc39.github.io/ecma262/
[HTML]
Anne van Kesteren; et al. HTML Standard. Living Standard. URL: https://html.spec.whatwg.org/multipage/
[STREAMS]
Adam Rice; Domenic Denicola; 吉野剛史 (Takeshi Yoshino). Streams Standard. Living Standard. URL: https://streams.spec.whatwg.org/
[WEBIDL]
Boris Zbarsky. Web IDL. URL: https://heycam.github.io/webidl/

Informative References

[FILE-SYSTEM-API]
Eric Uhrhane. File API: Directories and System. URL: http://dev.w3.org/2009/dap/file-system/file-dir-sys.html
[INDEXEDDB]
Nikunj Mehta; et al. Indexed Database API. URL: http://dvcs.w3.org/hg/IndexedDB/raw-file/tip/Overview.html
[IndexedDB-2]
Ali Alabbas; Joshua Bell. Indexed Database API 2.0. URL: https://w3c.github.io/IndexedDB/
[NOTIFICATIONS]
Anne van Kesteren. Notifications API Standard. Living Standard. URL: https://notifications.spec.whatwg.org/
[XHR]
Anne van Kesteren. XMLHttpRequest Standard. Living Standard. URL: https://xhr.spec.whatwg.org/