Promises page
Learn about JavaScript promises
NOTE: This section is currently under development. There are no exercises yet.
What is a promise
A promise represents the completion or failure of some
operation. Promises have methods (.then
)
that let you listen for when the operation completes:
const myPromise = operationThatCompletesInTheFuture();
myPromise.then( function success(){
console.log( "operation completed 😄" );
} );
You can also listen to when an operation fails:
const myPromise = operationThatCompletesInTheFuture();
myPromise.catch( function failure(){
console.log( "operation failed 😟" );
} )
For example, you might use fetch to make a request and want to know if a connection was established:
const request = fetch("https://dog.ceo/api/breeds/list/all");
request.then( function fulfilled(){
console.log( "connection established" );
});
request.catch( function rejected(){
console.log( "connection failed" );
} )
Success values
If the operation is successful, the operation can return
a value. For example, if fetch
establishes a value,
a response
object is provided.
const request = fetch("https://dog.ceo/api/breeds/list/all");
request.then( function(response){
console.log(response.status) // Logs 200
});
Setting a promise’s returning value is actually called resolving the promise’s value. This is because a promise’s value can only be set once.
Failure reasons
If the operation fails, a reason for the failure can be returned.
For example, if fetch
makes a request to a URL that doesn’t exist,
the reason
will be an TypeError object.
const request = fetch("http://foo.bar");
request.catch( function(reason){
console.log(`The request failed with message: ${reason.message}`) /// TypeError: Failed to Fetch
});
When a promise fails, it is called rejecting the promise.
Creating promises
You can create your own promises with new Promise()
!
The following example creates a promise whose value is
resolved to a random number after one second. We also listen to when
the value is resolved with then
and log a message.
const numberPromise = new Promise( function( resolve ){
setTimeout( function(){
resolve( Math.random() ); // Sets the value of the promise
}, 1000 );
})
numberPromise.then( function( number ){
console.log("number "+ number); // Logs "number 0.###"
} );
Notice:
new Promise( executor )
takes anexecutor
function that is called with aresolve
callback.resolve
is used to set the value of the
promise..then( onFulfilled )
takes anonFulfilled
function that is called when the promise is resolved.onFulfilled
is called with the promise’s resolved value.
Example Implementation
The following implements a simplified version of promises. It does not handle errors, or calling handlers at the end of the current run loop.
class MyPromise {
constructor( executor ) {
this._fulfilledHandlers = [];
this._state = "pending";
const resolve = (value) => {
this._value = value;
this._fulfill();
}
executor( resolve );
}
then(onFulfilled) {
this._fulfilledHandlers.push(onFulfilled);
if(this._state === "resolved") {
this._fulfill();
}
}
_fulfill(){
this._state = "resolved";
const handlers = this._fulfilledHandlers;
this._fulfilledHandlers = [];
handlers.forEach( handler => handler(this._value) );
}
}
const groupNamePromise = new MyPromise( function( resolve ){
setTimeout( function(){
resolve("Bitovi"); // Sets the value of the promise
}, 1000 );
})
groupNamePromise.then( function( groupName ){
console.log("groupName", groupName); // Logs "groupName Bitovi"
} );
You can also create promises that can be rejected.
The following
creates a random number every 100ms. If a number is greater than 0.9
, it will
resolve with that number. If 10 numbers are created that are all less than 0.9
,
the promise will be rejected.
const numberPromise = new Promise( function( resolve, reject ){
let count = 0;
const interval = setInterval( ()=> {
count++;
let number = Math.random();
if( number > 0.9 ) {
clearInterval( interval );
resolve( number );
}
if( count >= 10) {
clearInterval( interval );
reject( new Error("Unable to find a number greater than 0.9") );
}
},100);
})
numberPromise.then( function( number ){
console.log("number"+ number);
} );
numberPromise.catch( function( error ){
console.error(error);
} );
Shorthands
You can also create resolved and rejected promises with shorthands:
const numberPromise = Promise.resolve(123);
numberPromise.then(console.log) //-> 123
const numberPromise = Promise.reject( new Error("Borked") );
numberPromise.catch(console.log) //-> Error[Borked]
Creating promises from other promises
One of the most common uses of promises is to take a promise value and convert it to another promise value.
A promise’s .then( onFulfilled )
method always returns another outer promise. That outer promise
will take on the value of what the onFulfilled
function returns.
In the following example, notice how breedsPromise
resolves to an object of breeds. However,
the toCount
function is returning the number of breeds. This return value is used to
make countPromise
. countPromise
resolves to the total number of breeds.
const request = fetch("https://dog.ceo/api/breeds/list/all");
const breedsPromise = request.then( function( response ){
console.log( "response", response ) //-> Response{status, body}
return response.json();
});
const countPromise = breedsPromise.then( function toCount(breeds) {
console.log("breeds", breeds); //-> {message: {beagle,chow}}
return Object.keys( breeds.message ).length;
});
countPromise.then( (count) => {
console.log("count", count); //-> 91
})
Flattening promises
If a onFulfilled
function returns another promise, the outer promise actually takes on
the behavior of the returned promise. This was used in the above example. response.json()
returns a promise, so breedsPromise
took on the behavior of the result of response.json()
.
NOTE: The process of reducing nested observables into a single observable is often called flattening. This is what it’s called in RxJS's mergeAll.
The following shows using a delay()
function to delay when countPromise
completes by 3 seconds. delay
returns a promise that resolves after 3 seconds.
function delay(value){
return new Promise( (resolve) => {
setTimeout( function(){
resolve(value);
}, 3000);
});
}
const request = fetch("https://dog.ceo/api/breeds/list/all");
const breedsPromise = request.then( response => response.json() );
let startTime;
const countPromise = breedsPromise.then( function toCount(breeds) {
startTime = new Date();
return delay( Object.keys( breeds.message ).length );
});
countPromise.then( (count) => {
console.log("delay", new Date() - startTime ); // Logs 3000
console.log("count", count);
})
Throwing exceptions
If an onFulfilled
function throws an exception, this will reject the outer
promise returned by .then
.
For example, if toCount
mistakenly read breeds.data
instead of breeds.message
,
countPromise
would be rejected:
const request = fetch("https://dog.ceo/api/breeds/list/all");
const breedsPromise = request.then( function( response ){
console.log( "response", response ) //-> Response{status, body}
return response.json();
});
const countPromise = breedsPromise.then( function toCount(breeds) {
console.log("breeds", breeds); //-> {message: {beagle,chow}}
return Object.keys( breeds.data ).length;
});
countPromise.then( (count) => {
console.log("count", count);
})
countPromise.catch( (reason) => {
console.error(reason) //-> TypeError[Cannot convert undefined or null to object]
})
Promise providers
Promises are returned by many APIs such as:
The
fetch()
API returns a promise when a connection is established. You can also easily get a promise when the JSON response is complete:const myJSON = fetch("/my-data.json") .then( (response) => response.toJSON() )
Promise syntax
JavaScript even has special syntax for using promises:
async function getBreedsCount(){
const response = await fetch("https://dog.ceo/api/breeds/list/all");
const breeds = await response.json();
return Object.keys( breeds.message ).length;
}
const countPromise = getBreedsCount();
countPromise.then( (count) => {
console.log("count", count); //-> 91
});
for await(of) allows you to loop through an iterator that emits promises:
const response = await fetch(url); for await (const chunk of response.body.getReader() ) { responseSize += chunk.length; }
Timing
Promises' callback handlers are run in the
microtask queue
which is called at the end of the JavaScript
event loop. This means a few things.
First, callbacks of resolved promises are not called immediately. Instead they are called at the end of the current event loop.
The following:
- sets a timeout callback for
1ms
- runs code that should last longer than
20ms
- sets a fulfilled promise callback
const promise = Promise.resolve();
setTimeout( ()=> {
console.log("timeout callback");
},1);
var total = 0
for(var i = 0; i < 100000; i++) {
total += Math.sqrt(i) * (i % 2 === 0 ? 1 : -1)
}
promise.then( ()=> {
console.log("promise fulfilled");
});
// Logs:
// "promise fulfilled"
// "timeout callback"
Second, this also means that resolving
a promise
does not call all callbacks immediately as shown
in the following example:
let resolve;
const promise = new Promise( (pResolve)=> {
resolve = pResolve;
} )
promise.then(()=> {
console.log("promise fulfilled")
});
console.log("before resolve");
resolve();
console.log("after resolve");
// Logs:
// "before resolve"
// "after resolve"
// "promise fulfilled"
Why was this done? Consistency. As developers, you know that your callbacks will always be called sometime in the future. If callbacks were called immediately, this would have to be handled.
Chaining vs callbacks
Without Promises, the common way of handling asynchronous behavior was to pass a success and failure callback to functions as follows.
doSomething( (filesData) => {
processFiles(filesData.files, function(processedFilesData) => {
writeHTML(processedFilesData.writableFiles, function() {
console.log("completed!");
}, failureCallback);
}, failureCallback);
}, failureCallback);
This is hard to read and results in the classic callback pyramid of doom 🔥🔥🔥!
Fortunately, promises make this better, by making a much more linear process:
getFiles()
.then( (filesData) => {
return processFiles(filesData.files);
})
.then( (processedFilesData) => {
return writeHTML(processedFilesData.writableFiles);
}).
then( ()=> {
console.log("completed!")
})
.catch( failureCallback );
Promise queues
Often, you want to run a series of tasks, but those tasks might be optional. Making this work with promises can be tricky.
let promise = getFiles();
if(options.debug) {
promise = promise.then(printFiles)
}
promise = processFiles();
if(options.debug) {
promise = promise.then(printProcessWarnings)
}
promise = promise.then(writeHTML);
One way to simplify this is to use a promiseQueue that wires up functions to be called one after another.
function promiseQueue(functions){
var promise = functions.shift()();
var func;
while( functions.length ) {
func = functions.shift();
if(func) {
promise = promise.then(func);
}
}
return promise;
};
const promise = promiseQueue([
getFiles,
options.debug && printFiles,
processFiles,
options.debug && printProcessWarnings,
writeHTML
]);
Waiting for multiple async tasks to complete
Often, you might want to do two or more parallel tasks and then do something with the result. Promise.all takes multiple promises and returns another promise when they all complete.
For example, you might want to compare the number of hound
to dingo
breed images.
One way of doing that would be to get hounds and get dings and compare:
const time = new Date();
fetch("https://dog.ceo/api/breed/hound/images")
.then( response=> response.json() )
.then( (hounds) => {
return fetch("https://dog.ceo/api/breed/dingo/images")
.then( response => response.json() )
.then( dingos => { return {hounds, dingos} })
} )
.then( ({hounds, dingos}) => {
return {houndsCount: hounds.message.length, dingosCount: dingos.message.length}
})
.then( ({houndsCount, dingosCount}) => {
console.log(`${houndsCount} hounds, ${dingosCount} dingos,
time ${new Date() - time}`);
} );
But this would create one request after another. Slow.
Instead, use Promise.all()
to make both requests at the same time:
const time = new Date();
const houndsPromise = fetch("https://dog.ceo/api/breed/hound/images")
.then( response=> response.json() );
const dingosPromise = fetch("https://dog.ceo/api/breed/dingo/images")
.then( response => response.json() )
Promise.all([houndsPromise, dingosPromise])
.then( ([hounds, dingos])=> {
return {houndsCount: hounds.message.length, dingosCount: dingos.message.length}
})
.then( ({houndsCount, dingosCount}) => {
console.log(`${houndsCount} hounds, ${dingosCount} dingos,
time ${new Date() - time}`);
} );
Promises compared to alternatives
Promises compared to callbacks
Callback functions can be passed to a function. In the following example,
doSomething()
takes a completion callback:
doSomething(someArg, function onComplete(err, data) {
if(err) {
// handle error case
} else {
// handle success
}
});
This form of continuation passing is very common in Node.js.
Callback positives:
- Callbacks are lighter than promises - a function just needs to be created. Dispatching is also faster - a function just needs to be called.
Callback negatives:
- Callbacks only allow a single listener.
- Callbacks might be called immediately, which can create timing issues.
- Callbacks can create the pyramid of doom.
When to use callbacks instead of promises
If you are making something that needs to run extremely quickly and doesn’t need to be user friendly, callbacks might be a good solution. After all, it’s not difficult to "promisify" callback-based APIs. Many libraries do exactly this.
Promises compared to event streams
Event streams are any technology that might produce values
overtime. For example, listening to a click
event on the DOM
is a form of event stream:
document.body.addEventListener("click", (event) => {
console.log("Got a click event", event);
})
RxJS is a functional-reactive event stream library. The following
uses RxJS to create an event stream (subject
) that emits two random numbers:
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.2.1/rxjs.umd.js"></script>
<script type="typescript">
const {Subject} = rxjs;
const subject = new Subject<number>();
subject.subscribe({
next: (v) => console.log(`observerA: ${v}`)
});
subject.next(Math.random());
subject.next(Math.random());
subject.complete();
</script>
Modern browsers even provide their own stream primitive - ReadableStream
.
This lets you create a stream of events.
Event stream positives:
- Event streams can emit values over time. Promise can not do this.
- Event streams can be cancelled. There’s no way to do this through the promise API.
- Event streams often have utility libraries, making deriving new event streams from other event streams easy.
Event stream negatives:
- You must stop listening to an event stream or end the stream to avoid memory leaks.
- Event streams are often heavier than Promises.
- With the exception of
ReadableStream
(which isn’t in every environment yet), streams are not part of the JavaScript specification and are not present in every environment. Promises come for free for almost every user.
When to use streams instead of promises
If your system produces a single "event", returning a promise is generally better than returning an event stream. Most event stream libraries have ways of converting a promise to an event stream.
However, if your system produces multiple events, you have no choice but to use some form of event stream.