Chapter 4. Using Libraries and Frameworks
Before the ES6 Promise API existed, many JavaScript libraries and frameworks implemented their own version of Promises. Some libraries were written for the sole purpose of providing promises while established libraries like jQuery added them to handle their async APIs.
Promise libraries can act as polyfills in older web browsers and other environments where native promises are not provided. They can also supplement the standard API with a wide set of functions for managing promises. If your code only uses promises that you create you’re in a good position to choose a library and take full advantage of its extended API. And if you are handling promises that other libraries produced, you can wrap those promises with ones from your chosen library to access the additional features.
This chapter focuses on nonnative promise implementations. The majority of the chapter covers Bluebird, a fast and robust promise library. Although Bluebird is a compelling choice, there are other good options. For example, the Q promise library predates Bluebird and is widely used in applications and frameworks including AngularJS. Q and other libraries are not discussed in detail because this chapter is not a guide to choosing between libraries. It is an introduction to the enhancements that third-party libraries offer to demonstrate their value. The Promise implementation in jQuery is also discussed because of jQuery’s immense popularity. However, this is not a complete walk-through of either Bluebird or jQuery. These open source projects evolve rapidly, so refer to the official online documentation for full details of the current features.
Promise Interoperability and Thenables
Before diving into the details of specific libraries, let’s discuss how promises from different libraries can be used with one another. The basis of all interoperability between promise implementations is the thenable contract. Any object with a then(on
Fulfilled,
onRejected)
method can be wrapped by any standard promise implementation.
As Kris Kowal wrote when reviewing this chapter, “…regardless of what that method returns, regardless of what onFulfilled
and onRejected
return, and in fact regardless of whether onFulfilled
or onRejected
are executed synchronously or asynchronously, [then
] is sufficient for any of these Promise implementations to coerce the thenable into a well-behaved, always-asynchronous, always returning capital-P Promise, promise. This is particularly important when consuming promises from unreliable third parties, where unreliable can be as innocuous as a backward-incompatible version of the same library.”
Example 4-1 shows an example of a simple thenable object wrapped with a standard promise.
Example 4-1. Wrapping a thenable for interoperability
function
thenable
(
value
)
{
return
{
then
:
function
(
onfulfill
,
onreject
)
{
onfulfill
(
value
);
}
};
}
var
promise
=
Promise
.
resolve
(
thenable
(
'voila!'
));
promise
.
then
(
function
(
result
)
{
console
.
log
(
result
);
});
// Console output:
// voila!
Although it is unlikely you will encounter such a sparse thenable in your own code, the same concept applies to wrapping promises from other implementations to work as the promise implementation that your code prefers.
The Bluebird Promise Library
Bluebird is an open source promise library with a rich API and excellent performance. The Bluebird GitHub repo includes benchmarks that show it outperforming other implementations, including the native version in the V8 JavaScript engine used by Node.js and Google Chrome. Bluebird’s author Petka Antonov says native implementations are more focused on matching behavior specifications than performance optimization, which allows carefully tuned JavaScript to outperform native code.
Bluebird offers many other features including elegant ways of managing execution context, wrapping Node.js APIs, working with collections of promises, and manipulating fulfillment values.
Loading Bluebird
When Bluebird is included in a web page using a script
tag, it overwrites the global Promise object by default with its own version of Promise. Bluebird can also be loaded in the browser in other ways, such as an AMD module using require.js, and it is available as an npm package for use in Node.js.
The Bluebird Promise object can serve as a drop-in replacement or polyfill for the ES6 Promise. When the Bluebird script is loaded in a web browser it overwrites the global Promise object. However, you can use Promise.noConflict()
after loading Bluebird to restore the global Promise object to its previous reference in order to run Bluebird side by side with native promises. As explained in the earlier section on interoperability and thenables, you can treat other promise implementations as Bluebird promises by wrapping them using [Bluebird Promise].resolve(promise)
. In Example 4-2, Bluebird wraps a native promise to expose functions that reveal its state.
Example 4-2. Wrap a native promise with a Bluebird promise
// Assume bluebird has been loaded using <script src="bluebird.js"></script>
var
Bluebird
=
Promise
.
noConflict
();
// Restore previous reference to Promise
var
nativePromise
=
Promise
.
resolve
();
// Native Promise
var
b
=
Bluebird
.
resolve
(
nativePromise
);
// Wrap native promise with Bluebird promise
// Force event loop to turn
setTimeout
(
function
()
{
console
.
log
(
'Pending? '
+
b
.
isPending
());
// Pending? false
console
.
log
(
'Fulfilled? '
+
b
.
isFulfilled
());
// Fulfilled? true
console
.
log
(
'Rejected? '
+
b
.
isRejected
());
// Rejected? false
},
0
);
The remaining examples in this chapter that relate to Bluebird assume that all promises are Bluebird promises.
Managing Execution Context
Callbacks frequently need access to variables in their enclosing scope. Two common ways of accessing those variables are shown in the configure
and print
methods in Example 4-3. Both access the pageSize
property of a printer object.
Example 4-3. Using the enclosing scope through function.bind() or aliasing
var
printer
=
{
pageSize
:
'US LETTER'
,
connect
:
function
()
{
// Return a promise that is fulfilled when a connection
// to the printer is established
},
configure
:
function
(
pageSize
)
{
return
this
.
connect
().
then
(
function
()
{
console
.
log
(
'Setting page size to '
+
pageSize
);
this
.
pageSize
=
pageSize
;
}.
bind
(
this
));
// Using bind to set the context
},
:
function
(
job
)
{
// Aliasing the outer context
// _this, that, and self are some other common alias names
var
me
=
this
;
return
this
.
connect
().
then
(
function
()
{
console
.
log
(
'Printing job using page size '
+
me
.
pageSize
);
});
}
};
printer
.
configure
(
'A4'
).
then
(
function
()
{
return
printer
.
(
'Test page'
);
});
// Console output:
// Setting page size to A4
// Printing job using page size A4
The configure
method uses bind(this)
to share its context with the inner callback. The print
method aliases the outer context to a variable called me
in order to access it inside the callback.
Bluebird offers an alternative way of exposing the enclosing scope by adding a promise.bind()
method that sets the context for all subsequent callbacks used in a promise chain, as shown in Example 4-4.
Example 4-4. Setting callback contexts using promise.bind()
printer
.
shutdown
=
function
()
{
this
.
connect
().
bind
(
this
).
then
(
function
()
{
// bluebird.bind not function.bind
console
.
log
(
'First callback can use '
+
this
.
paperSize
);
}).
then
(
function
()
{
console
.
log
(
'And second callback can use '
+
this
.
paperSize
);
});
};
// Console.output:
// First callback can use A4
// And second callback can use A4
Using bluebirdPromise.bind()
has an advantage over the previous two solutions because it removes the call to bind
for individual functions in a long chain and avoids adding a reference to the enclosing scope of each callback.
The effect of bind
applies to all subsequently chained promises, even those on a promise that a function returns. To avoid leaking objects used as the context in bind
, you can mask the effect by calling bind
again before returning a bound promise chain from a function. This practice is even more important if the function is being consumed as a third-party library.
Example 4-5 shows an updated version of the printer.shutdown()
method that masks the printer context that the callbacks inside it use.
Example 4-5. Hiding the bound context from calling code
printer
.
shutdown
=
function
()
{
return
this
.
connect
().
bind
(
this
).
then
(
function
()
{
//...
}).
then
(
function
()
{
//...
}).
bind
(
null
);
// mask the previous binding
};
printer
.
shutdown
().
then
(
function
()
{
console
.
log
(
'Not running in the context of the printer: '
+
this
!==
printer
);
});
// Console.output:
// This code is not running in the context of the printer: true
Wrapping Node.js Functions
Node.js has a standard way of using callbacks in async functions. The node-style expects a callback as the last argument of a function. The first parameter of the callback is an error object followed by any additional parameters. Example 4-6 shows a version of the loadImage
function implemented in this style.
Example 4-6. Node-style callback
function
loadImageNodeStyle
(
url
,
callback
)
{
var
image
=
new
Image
();
image
.
src
=
url
;
image
.
onload
=
function
()
{
callback
(
null
,
image
);
};
image
.
onerror
=
function
(
error
)
{
callback
(
error
);
};
}
loadImageNodeStyle
(
'labyrinth_puzzle.png'
,
function
(
err
,
image
)
{
if
(
err
)
{
console
.
log
(
'Unable to load image'
);
return
;
}
console
.
log
(
'Image loaded'
);
});
Bluebird provides a convenient function named promisify
that wraps node-style functions with ones that return a promise, as shown in Example 4-7.
Example 4-7. Using promisify to wrap a node-style function
var
loadImageWrapper
=
Bluebird
.
promisify
(
loadImageNodeStyle
);
var
promise
=
loadImageWrapper
(
'computer_problems.png'
);
promise
.
then
(
function
(
image
)
{
console
.
log
(
'Image loaded'
);
}).
catch
(
function
(
error
)
{
console
.
log
(
'Unable to load image'
);
});
The loadImageWrapper
function accepts the same url
argument as the original load
Image
NodeStyle
function but does not require a callback. Using promisify
creates a callback internally and correctly wires it to a promise. If the callback receives an error the promise is rejected. Otherwise the promise is fulfilled with any additional arguments passed to the callback.
Standard promises cannot be fulfilled by more than one value. However, some node-style callbacks expect more than one value when an operation succeeds. In this case you can instruct promisify
to fulfill the promise with an array containing all the arguments passed to the function except the error argument, which is not relevant. The array can be converted back to individual function arguments using Bluebird’s promise.spread()
method. Example 4-8 shows an example of a node-style function that provides multiple pieces of information about a user’s account.
Example 4-8. Converting arrays into individual arguments using promise.spread()
function
getAccountStatus
(
callback
)
{
var
error
=
null
;
var
enabled
=
true
;
var
lastLogin
=
new
Date
();
callback
(
error
,
enabled
,
lastLogin
);
// Callback has multiple values on success
}
var
fulfillUsingAnArray
=
true
;
var
wrapperFunc
=
Bluebird
.
promisify
(
getAccountStatus
,
fulfillUsingAnArray
);
// Without using spread
wrapperFunc
().
then
(
function
(
status
)
{
var
enabled
=
status
[
0
];
var
lastLogin
=
status
[
1
];
// ...
});
// Using spread
wrapperFunc
().
spread
(
function
(
enabled
,
lastLogin
)
{
// ...
});
Using spread
in this example allows the enabled
and lastLogin
values to be clearly specified without the need to extract them from an array. Use spread
to simplify the code whenever a promise is fulfilled with an array whose length and order of elements are known.
ES6 includes a feature called destructuring that can assign values from an array to individual variables. This feature is described in “Destructuring”.
If you want to specify the context in which the node-style function runs, you can pass the context as an argument to promisify
or bind the context to the function before wrapping it with promisify
, as shown in Example 4-9.
Example 4-9. Specifying the execution context for a wrapped function
var
person
=
{
name
:
'Marie'
,
introNodeStyle
:
function
(
callback
)
{
var
err
=
null
;
callback
(
err
,
'My name is '
+
this
.
name
);
}
};
var
wrapper
=
Bluebird
.
promisify
(
person
.
introNodeStyle
);
wrapper
().
then
(
function
(
greeting
)
{
console
.
log
(
'promisify without second argument: '
+
greeting
);
});
var
wrapperWithPersonArg
=
Bluebird
.
promisify
(
person
.
introNodeStyle
,
person
);
wrapperWithPersonArg
().
then
(
function
(
greeting
)
{
console
.
log
(
'promisify with a context argument: '
+
greeting
);
});
var
wrapperWithBind
=
Bluebird
.
promisify
(
person
.
introNodeStyle
.
bind
(
person
));
wrapperWithBind
().
then
(
function
(
greeting
)
{
console
.
log
(
'promisify using function.bind: '
+
greeting
);
});
// Console output:
// promisify without second argument: Hello my name is
// promisify with a context argument: Hello my name is Marie
// promisify using function.bind: Hello my name is Marie
Only the wrappers using a bound function or where the context was provided as a second argument include a name. All the wrappers call person.introNodeStyle()
, which builds a string containing this.name
. However, the first wrapper created with an undefined second argument was run in the root object scope (the window object in a web browser), which does not have a name
property. The next wrapper specifies the context by passing it as the second argument to promisify
. And the last one used the function’s bind
method to set the context to an object literal.
Caution
Be careful when wrapping functions that are intended to run as methods (i.e., in the context of a certain object.) Use the function’s bind
or an equivalent wrapper to ensure the method is run in the expected context. Running methods in the wrong context may produce runtime errors or unexpected behavior.
Working with Collections of Promises
Bluebird provides promise-enabled versions of the map
, reduce
, and filter
methods similar to the ones available for standard JavaScript arrays. Example 4-10 shows the filter
and reduce
methods at work.
Example 4-10. Using a promise-enabled filter and reduce
function
sumOddNumbers
(
numbers
)
{
return
numbers
.
filter
(
function
removeEvenNumbers
(
num
)
{
return
num
%
2
==
1
;
}).
reduce
(
function
sum
(
runningTotal
,
num
)
{
return
runningTotal
+
num
;
},
0
);
}
// Use sumOddNumbers as a synchronous function
var
firstSum
=
sumOddNumbers
([
1
,
2
,
3
,
4
]);
console
.
log
(
'first sum: '
+
firstSum
);
// Use sumOddNumbers as an async function
var
promise
=
Bluebird
.
resolve
([
5
,
6
,
7
,
8
]);
sumOddNumbers
(
promise
).
then
(
function
(
secondSum
)
{
console
.
log
(
'second sum: '
+
secondSum
);
});
// Console output:
// first sum: 4
// second sum: 12
The sumOddNumbers
function accepts an array of numbers and uses filter
to remove any even numbers. Then reduce
is used to add together the remaining values. The function works regardless of whether it is passed a standard array or a promise that an array fulfilled. These promise-enabled methods allow you to write async code that looks identical to the synchronous equivalent.
Although the synchronous and async code looks the same and produces the same result, the execution sequence may differ. The promise-enabled map
, reduce
, and filter
methods invoke their callbacks for each value as soon as possible. When the array contains a promise, the callback is not invoked for that element until the promise is resolved. For map
and filter
that means the callbacks can receive values in a different order than they appear in the array. Example 4-11 shows a map
passing values to the callback out of order.
Example 4-11. Eager invocation of aggregate functions
function
resolveLater
(
value
)
{
return
new
Bluebird
(
function
(
resolve
,
reject
)
{
setTimeout
(
function
()
{
resolve
(
value
);
},
1000
);
});
};
var
numbers
=
Bluebird
.
resolve
([
1
,
resolveLater
(
2
),
3
]);
console
.
log
(
'Square the following numbers...'
);
numbers
.
map
(
function
square
(
num
)
{
console
.
log
(
num
);
return
num
*
num
;
}).
then
(
function
(
result
)
{
console
.
log
(
'The squares of those numbers are...'
);
console
.
log
(
result
.
join
(
', '
));
});
// Console output:
// Square the following numbers...
// 1
// 3
// 2
// The squares of those numbers are...
// 1, 4, 9
When map
is invoked it receives an array whose second element is an unresolved promise. The other two elements are numbers that are immediately passed to the the square
callback. After fulfilling the second promise, its value is passed to square
. Once square
processes all the values, an array that is identical to the one that the synchronous array.map()
function would return resolves the promise returned by map
.
Since using array.map()
or Bluebird.map()
in this example produces the same result
, it doesn’t matter what order the values are passed to the callbacks. That only works as long as the callback used for map
does not have any side effects. The map
function is meant to convert one value to another using a callback. Adding side effects to the map
callback conflicts with the intended use. The same thing applies to the reduce
and filter
functions. Avoid trouble by keeping any callbacks these functions use free from side effects.
Manipulating Fulfillment Values
When chaining together promises to execute a series of steps, the fulfillment value of one step often provides a value needed in the next step. This progression generally works well, but sometimes multiple subsequent steps require the same value. In that case you need a way to expose the fulfillment value to additional steps.
Imagine a series of database commands that all require a connection object. If the connection is obtained through a promise in the chain it will not be available to other steps in the chain by default. You can expose the connection to other steps by assigning it to a variable in the enclosing scope, as shown in Example 4-12.
Example 4-12. Exposing a fulfillment value using the enclosing scope
var
connection
;
// Declare in outer scope for use in multiple functions
getConnection
().
then
(
function
(
con
)
{
connection
=
con
;
return
connection
.
insert
(
'student'
,
{
name
:
'Bobby'
});
}).
then
(
function
()
{
return
connection
.
count
(
'students'
);
}).
then
(
function
(
count
)
{
console
.
log
(
'Number of students: '
+
count
);
return
connection
.
close
();
});
The promise chain in the example consists of three callbacks. The first callback inserts a student, the second callback fetches the number of students, and the third reports the number in the console. In order for all three callbacks to use the connection object, the fulfillment value from getConnection
is assigned to the connection
variable in the enclosing scope.
There are ways to expose the connection object to the other callbacks without creating a variable in the outer scope. Bluebird promises have a return
method that returns a new promise that is resolved by the argument it is given. Example 4-13 is a revised snippet using return
to pass the connection to the second callback.
Example 4-13. Passing on a value using promise.return()
getConnection
().
then
(
function
(
connection
)
{
return
connection
.
insert
(
'student'
,
{
name
:
'Bobby'
})
.
return
(
connection
);
}).
then
(
function
(
connection
)
{
...
For this scenario you could also use the tap
method of a Bluebird promise to get the connection object to the second callback. The tap
method allows you to insert a callback into the promise chain while passing the fulfillment value it receives on to the next callback.
Example 4-14. Passing on a value using promise.tap()
getConnection
().
tap
(
function
(
connection
)
{
return
connection
.
insert
(
'student'
,
{
name
:
'Bobby'
});
}).
then
(
function
(
connection
)
{
//...
Think of tap
as tapping into a line without interfering with the existing flow. Use tap
to add supplementary functions into a promise chain. A practical use for tap
would be adding a logging statement into a promise chain, as shown in Example 4-15.
Example 4-15. Supplementing a chain with promise.tap()
function
countStudents
()
{
return
getConnection
().
then
(
function
(
connection
)
{
return
connection
.
count
(
'students'
);
}).
tap
(
function
(
count
)
{
console
.
log
(
'Number of students: '
+
count
);
});
}
countStudents
().
then
(
function
(
count
)
{
if
(
count
>
24
)
console
.
log
(
'Classroom has too many students'
);
});
// Console output:
// Number of students: 25
// Classroom has too many students
The call to tap
in the countStudents
function can be added or removed without affecting the outcome of the function.
Using return
or tap
masks the fulfillment value that the callback would otherwise return. That worked well in the previous examples because the results of connection.
insert()
or console.log()
were not needed. In situations where they are needed, you can supplement the original fulfillment value with additional items in a callback by passing them in an array to Promise.all()
, as shown in Example 4-16. Then the items in the array can be split into separate arguments of a callback using spread
.
Example 4-16. Passing in multiple values with Promise.all()
getConnection
().
then
(
function
(
connection
)
{
var
promiseForCount
=
connection
.
count
(
'students'
);
return
Promise
.
all
([
connection
,
promiseForCount
]);
}).
spread
(
function
(
connection
,
count
)
{
console
.
log
(
'Number of students: '
+
count
);
return
connection
.
close
();
});
Promises in jQuery
In jQuery, deferred objects represent async operations. A deferred object is like a promise whose resolve
and reject
functions are exposed as methods. Example 4-17 shows a loadImage
function using a deferred object.
Example 4-17. Simple deferred object in jQuery
function
loadImage
(
url
)
{
var
deferred
=
jQuery
.
Deferred
();
var
img
=
new
Image
();
img
.
src
=
url
;
img
.
onload
=
function
()
{
deferred
.
resolve
(
img
);
};
img
.
onerror
=
function
(
e
)
{
deferred
.
reject
(
e
);
};
return
deferred
;
}
The standard Promise API encapsulates the resolve
and reject
functions inside the promise. For example, if you have a promise object p
, you cannot call p.resolve()
or p.reject()
because those functions are not attached to p
. Any code that receives a reference to p
can attach callbacks using p.then()
or p.catch()
but the code cannot control whether p
gets fulfilled or rejected.
By encapsulating the resolve
and reject
functions inside the promise you can confidently expose the promise to other pieces of code while remaining certain the code cannot affect the fate of the promise. Without this guarantee you would have to consider all code that a promise was exposed to anytime a promise was resolved or rejected in an unexpected way.
Using deferreds does not mean you have to expose the resolve
and reject
methods everywhere. The deferred object also exposes a promise that can be given to any code that should not be calling resolve
or reject
.
Compare the two functions in Example 4-18. The first is a revised version of loadImage
that returns deferred.promise()
and the second is the equivalent function implemented with a standard promise.
Example 4-18. Deferred throws synchronous errors
function
loadImage
(
url
)
{
var
deferred
=
jQuery
.
Deferred
();
// ...
return
deferred
.
promise
();
}
function
loadImageWithoutDeferred
(
url
)
{
return
new
Promise
(
function
resolver
(
resolve
,
reject
)
{
var
image
=
new
Image
();
image
.
src
=
url
;
image
.
onload
=
function
()
{
resolve
(
image
);
};
image
.
onerror
=
reject
;
});
}
The main difference between the two functions is that the function used as a deferred could throw a synchronous error while any errors thrown inside the function with the Promise constructor are caught and used to reject the promise. Promises created from jQuery deferreds do not conform to the standard ES6 Promise API or behavior. Some method names on jQuery promises differ from the spec; for example, [jQueryPromise].fail()
is the counterpart to [standardPromise].catch()
.
A more important difference is in handling errors in the onFulfilled
and onRejected
callbacks. Standard promises automatically catch any errors thrown in these callbacks and convert them into rejections. In jQuery promises, these errors bubble up the call stack as uncaught exceptions.
Also, jQuery will invoke an onFulfilled
or onRejected
callback synchronously if settling a promise before the callback is registered. This creates the problems with multiple execution paths described in Chapter 1.
For more differences between standard promises and the ones jQuery provides, refer to a document written by Kris Kowal titled Coming from jQuery.
Some developers may prefer the style of deferred objects or find them easier to understand. However, a more significant case for using a deferred is in a situation where you cannot resolve the promise in the place it is created.
Suppose you are using a web worker to perform long-running tasks. You can use promises to represent the outcome of the tasks. The code that receives the response from the web worker will resolve the promise so it needs access to the appropriate resolve
and reject
functions. Example 4-19 demonstrates this.
Example 4-19. Managing web worker results with deferred objects
// Contents of task.js
onmessage
=
function
(
event
)
{
postMessage
({
status
:
'completed'
,
id
:
event
.
data
.
id
,
result
:
'some calculated result'
});
};
// Contents of main.js
var
worker
=
new
Worker
(
'task.js'
);
var
deferreds
=
{};
var
counter
=
0
;
worker
.
onmessage
=
function
(
msg
)
{
var
d
=
deferreds
[
msg
.
data
.
id
];
d
.
resolve
(
msg
.
data
.
result
);
};
function
background
(
task
)
{
var
id
=
counter
++
;
var
deferred
=
jQuery
.
Deferred
();
deferreds
[
id
]
=
deferred
;
// Store deferred for later resolution
console
.
log
(
'Sending task to worker: '
+
task
);
worker
.
postMessage
({
id
:
id
,
task
:
task
});
return
deferred
.
promise
();
// Only expose promise to calling code
}
background
(
'Solve for x'
).
then
(
function
(
result
)
{
console
.
log
(
'The outcome is... '
+
result
);
}).
fail
(
function
(
err
)
{
console
.
log
(
'Unable to complete task'
);
console
.
log
(
err
);
});
// Console output:
// Sending task to worker: Solve for x
// The outcome is... some calculated result
Example 4-19 shows the contents of two files: tasks.js
for the web worker and main.js
for the script that launches the worker and receives the results. The worker script is extremely simple for this example. Any time it receives a message it replies with an object containing the id
of the original request and a hard-coded result. The background
function in the main script returns a resolved promise once the worker sends a “completed” message for that task. Since processing the completed message occurs outside the background
function that creates the promise, a deferred object is used to expose a resolve
function to the onCompleted
callback.
Note
For detailed information on web workers, refer to Web Workers: Multithreaded Programs in JavaScript by Ido Green (O’Reilly.)
One final note for this section: if you wish to use a deferred object without jQuery, it is easy to create one using the standard Promise API, as shown in Example 4-20.
Example 4-20. Creating a deferred object using a standard Promise constructor
function
Deferred
()
{
var
me
=
this
;
me
.
promise
=
new
Promise
(
function
(
resolve
,
reject
)
{
me
.
resolve
=
resolve
;
me
.
reject
=
reject
;
});
}
var
d
=
new
Deferred
();
Summary
Libraries offer an extended set of features for working with promises. Many of these are convenience functions that save you from mixing the plumbing with your code. This chapter covered a number of features that the Bluebird library provides, although Q and other libraries offer similar functionality and are also popular among developers. It also explained the deferred objects jQuery uses. These objects expose promises that have some significant behavioral differences compared to the ES6 standard.
Get JavaScript with Promises now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.