Use bluebird to implement more powerful promises

Mondo History Updated on 2024-02-09

The preferred implementation of j**ascript apps.

Promises are an important concept in j**ascript development. As defined by the Promises A+ specification, a promise represents the end result of an asynchronous operation. The way to interact with a promise is through its then(onfulfilled, onrejected) method, which registers the method that handles the end result of the promise. The asynchronous operation represented by a promise may succeed or fail. When successful, the onfulfilled method accepts a value as the final result; When it fails, the method onrejected accepts the reason for the operation failure.

A promise can be in one of three states: pending, which means that the asynchronous operation corresponding to the promise is still in progress; Fulfilled indicates that the asynchronous operation has completed successfully; Rejected indicates that the asynchronous operation could not be completed. The then method of the promise object accepts two parameters, which are the satisfied state and the rejected state. The first argument of the method of the satisfied state is the final result value of the promise; The first argument to the Rejected method is the reason why the promise was rejected.

Promises are part of the ECMAScript 6 specification, and there are already a number of third-party libraries that provide implementations related to promises. The Promises A+ specification simply defines the then method, which is intended for interoperability between different implementations, with a focus on general use cases for promises. The ecmascript 6 specification additionally defines other methods for promises, including promisesall、promise.race、promise.reject and promiseresolve, etc. Different promise implementations can be used in nodejs and browser environments. Bluebird, described in this article, is a feature-rich and high-performance promise implementation.

Depending on the operating environment, there can be different installation methods. In a nodejs application, you can install it directly using npm install bluebird. In the browser app, you can publish a version of the jascript file directly or install it using bower. The example in this article is based on the nodejs environment, and the corresponding version is LTS 89.Version 4, using the syntax of ECMASCRIPT 6. In a nodejs environment, you can start using the promise object provided by bluebird by const promise = require('bluebird').

The first question when using Bluebird as a promise implementation is how to create a promise object. In fact, most of the time you don't need to explicitly create promises. Many third-party libraries already return promise objects or thenable objects that contain then methods, which can be used directly.

One useful feature of Bluebird is to wrap existing APIs that don't use promises into new APIs that return promises. Most of the standard library APIs of NodeJS and many third-party library APIs use the mode of **method, that is, when performing an asynchronous operation, you need to pass in a **method to accept the execution result of the operation and possible errors. For such methods, Bluebird can easily convert them to the form of promises.

For example, fs. in nodejs that reads filesreadfile method, the common usage patterns are shown in Listing 1. The problem with using the method is that it can lead to too many nested layers, creating what is known as callback hell.

Listing 1fs. in nodejsThe basic usage of the readfile method.

const fs = require('fs'),path = require('path');fs.readfile(path.join(__dirname, 'sample.txt'), 'utf-8', (err, data) => else })
Bluebird's promiseThe promisifyall method creates a version of a promise for all methods in an object's properties. The names of these newly created methods are suffix "async" after the names of existing methods. The implementation in Listing 1 can be changed to the form of a promise in Listing 2. The new readfileasync method corresponds to the existing readfile method, but the return value is a promise object. In addition to readfile, other methods in FS also have corresponding versions of async, such as writefileasync and fstatasync.

Listing 2Use promisepromisifyall to convert methods.

const promise = require('bluebird'),fs = require('fs'),path = require('path');promise.promisifyall(fs);fs.readfileasync(path.join(__dirname, 'sample.txt'), 'utf-8').then(data => console.log(data)).catch(err => console.error(err));
If you don't want to automatically convert all of an object's methods to the form of a promise, you can use a promisepromisify to transform a single method, such as a promisepromisify(require(“fs”).readfile)。For a nodejs-formatted method, you can use a promiseFromCallback converts it into a promise. The result of the method determines the state of the promise.

For an existing value, you can use promiseThe resolve method converts it into a promise object with a satisfied state. Again, use promiseThe reject method can create a promise object with a state of Rejected from a given reason for rejection. Both of these methods are a quick way to create a promise object.

If none of these methods meet the needs of creating a promise, you can use the promise constructor. The argument to the constructor is a method that accepts two methods as arguments, which are used to mark the promise as satisfied or rejected, respectively. In Listing 3, the state of the created promise depends on whether the generated random number is greater than 05。

Listing 3Create a promise

const promise = require('bluebird');const promise = new promise((resolve, reject) => else `)promise.then(console.log).catch(console.error);
In the previous example, we saw the basic usage of the promise object. where the then method is used to add a method that handles the result of the promise. You can add methods for both the Satisfied and Rejected states. The spread method works similarly to then. When the result value of the promise is an array, you can use the spread method to flatten the elements of the array as different parameters of the ** method. In Listing 4, the value of the promise is an array of 3 values, corresponding to each of the 3 parameters of the processing method.

Listing 4Spread usage example.

promise.resolve([1, 2, 3]).spread((v1, v2, v3) => console.log(v1 + v2 + v3));
The catch method is used to add a rejected state. There are two ways to use the catch method. The first way only adds the ** method, which catches all error cases; The second way is to filter errors using error object types or predicates. The error method works similarly to catch, but error only handles the actual error object. The throw statement in j**ascript can throw any value, not necessarily an error object. Any value thrown by throw will be caught, but only error objects will be handled by error. In Listing 5, the error added handler is not called.

Listing 5error usage example.

promise.reject('not an error').error(err => console.log('should not appear'));
Finally, the method will be called regardless of the final state of the promise. Catch in Listing 6 will only catch TypeError, while the logic in finally will always be called. Listing 6catch and finally use examples.

promise.reject(new typeerror('some error')).catch(typeerror, console.error).finally(()=> console.log('done'));
For a promise object, you can use the methods it provides to view its internal state.

iSpending checks whether the status of the promise is in progress. isfulfilled checks if the status of the promise is satisfied. isrejected checks whether the status of the promise is rejected. iscancelled checks if the promise was canceled. value gets the result of the promise being satisfied. reason to get the reason why the promise was rejected. You can use the cancel method to cancel a promise, indicating that you are no longer interested in the outcome of the promise. When a promise is canceled, none of its methods will be called. The cancellation function is disabled by default and requires the use of promisesconfig to enable the feature. It should be noted that canceling a promise only means that the promise's method is not called, and does not automatically cancel the ongoing asynchronous operation, such as not automatically terminating the ongoing XHR request. If you need to add a custom cancellation logic, you can add a third parameter, oncancel, to register the method when the promise is canceled.

Listing 7Use the promise's cancellation feature.

promise.config();promise.delay(1000, 'hello').then(console.log).cancel();
The previous examples were for a single promise. In practice, it is often necessary to interact with multiple promises. For example, if we read a single file and return a promise object, we need to wait for several files to succeed before performing other operations. promise.All can accept an iterable argument and return a new promise. The resulting promise will only be satisfied if all the promise objects contained in the iterable are satisfied; An error in any promise will result in the result of the promise being rejected. The end result of a new promise is an array containing the results of the promise in the iterable. Any value can be included in the iterable. If the value is not a promise, its value appears directly in the result and does not need to wait for the promise to complete. In Listing 8, we use promisesall to wait for the 3 read file operations to complete.

Listing 8 promise.ALL usage examples.

promise.all([fs.readfileasync(path.join(__dirname, '1.txt'), 'utf-8'),fs.readfileasync(path.join(__dirname, '2.txt'), 'utf-8'),fs.readfileasync(path.join(__dirname, '3.txt'), 'utf-8')])then(results => console.log(results.join(', ')))catch(console.error);
In many cases, you need to convert objects in an iterable into promises and wait for those promises to complete. For such a scenario, you can use a promisemap。The first parameter of the map method is an iterable object, the second parameter is a method that is converted to a promise, and the third parameter is an optional configuration object that can be used to control the number of promises running at the same time using the property concurrency. In Listing 9, we use promisesmap converts an array containing the name of the file into a promise object that reads the contents of the file, and then waits for the read operation to complete. The functionality is the same as Listing 8, but the implementation is simpler.

Listing 9 promise.Examples of use for map.

promise.map(['1.txt', '2.txt', '3.txt'],name => fs.readfileasync(path.join(__dirname, name), 'utf-8')).then(results => console.log(results.join(', ')))catch(console.error);
promise.The role of the mapseries is the same as that of the promiseMaps are the same, except that the mapseries iterates through each element in the order in which it is available.

When you have multiple promises, you can use a promise if you only need to wait for some of them to be completedsome and specify the number of promises to be completed. In Listing 10, you only need to wait for the 2 file read operations to complete.

Listing 10 promise.Examples of the use of some.

promise.some([fs.readfileasync(path.join(__dirname, '1.txt'), 'utf-8'),fs.readfileasync(path.join(__dirname, '2.txt'), 'utf-8'),fs.readfileasync(path.join(__dirname, '3.txt'), 'utf-8')],2).then(results => console.log(results.join(', ')))catch(console.error);
promise.Any is equivalent to using a promisesome and set the quantity to 1, but promiseThe result of any is not an array of length 1, but a specific single value. promise.Race works similarly to any, but the result of a race can be a rejected promise. Therefore, the recommended practice is to use any.

promise.The filter can wait for multiple promises to be completed and filter the results. It actually works the same way as in a promisemap and then use the array's filter method to filter. In Listing 11, promiseFilters are used to filter out files with a content length of 1 or less.

Listing 11 promise.Example of the use of filter.

promise.filter([fs.readfileasync(path.join(__dirname, '1.txt'), 'utf-8'),fs.readfileasync(path.join(__dirname, '2.txt'), 'utf-8'),fs.readfileasync(path.join(__dirname, '3.txt'), 'utf-8')],value => value.length > 1).then(results => console.log(results.join(', ')))catch(console.error);
promise.each processes the elements in the iterable in turn. The processing method can also return a promise object to represent the asynchronous processing. The next element is processed only after the previous element has been processed.

promise.reduce reduces the results of multiple promises to a single value. It works like the reduce method of an array, but can handle promise objects. In Listing 12, the first parameter is an array of pending file names; The second parameter is the method of the accumulation operation, total represents the current accumulation value, which is used to add the length of the current file that is read; The third parameter is the initial value of the accumulation. As you can see from **, the cumulative operation can also return a promise object. promise.Reduce waits for the promise to complete.

Listing 12 promise.Example of use for reduce.

promise.reduce(['1.txt', '2.txt', '3.txt'],(total, name) => ,0).then(result => console.log(`total size: $catch(console.error);
promise.all is used to handle a dynamic number of promises, promisesJoin is used to handle a fixed number of unrelated promises.

promise.method is used to encapsulate a method so that it returns a promise. promise.Try is used to encapsulate a method call that may cause problems, and to determine the state of the promise based on the result of the call.

If you use resources that need to be freed in the promise, such as database connections, it is not easy to ensure that these resources are freed. The general practice is to add resource release logic to finally. But when promises are nested within each other, it's easy to get resources that aren't released. Bluebird provides a more robust way to manage resources, using disposers and promisesusing method.

The resource releaser is represented by a disposer object, which is created through the disposer method of the promise. A create-time parameter is a method used to free up resources. The first argument to the method is a resource object, and the second argument is a promiseusing the resulting promise object. The disposer object can be passed to the promiseusing to ensure that its resource release logic is executed.

In Listing 13, connection represents a database connection and db represents a database. db's connect method creates a database connection that returns a promise object. Connection's query method represents the execution of a database query, and the result returned is also a Promise object. connection's close method closes the database connection. When used, use disposer on top of the promise object returned by the connect method to create a disposer object, and the corresponding resource release logic is to call the close method. Then you can pass the promiseusing to use the connection object.

Listing 13promises.

const promise = require('bluebird');class connection close() class db }const disposer = new db().connect().disposer(connection => connection.close())promise.using(disposer, connection => connection.query())then(console.log).catch(console.error);
promise.The promise returned by delay transitions to the satisfied state after the specified time, with the specified value as the result. promise.A timeout is used to add a timeout to an existing promise. If an existing promise is not completed within the specified timeout period, the resulting promise will generate a timeouterror error.

In Listing 14, the first delay method outputs hello after 1 second. The second method throws a TimeOutError error because the delay of 1 second exceeds the timeout of 500 milliseconds.

Listing 14 promise.delay and promiseExamples of use for timeout.

promise.delay(1000, 'hello').then(console.log);promise.delay(1000, 'hello').timeout(500, 'timed out').then(console.log).catch(console.error);
The promise also includes some practical methods. Tap and tapcatch are used to view the results in the promise and the errors that occur, respectively. The handling of these two methods does not affect the outcome of the promise and is suitable for logging. call is used to call methods in the promise result object. get is used to get the value of a property in the promise result object. return is used to change the outcome of a promise. throw is used to throw an error. CatchReturn is used to change the value of a promise after the error has been caught. CatchThrow is used to catch an error and then throw a new one.

*Examples of the use of these practical methods are given in Listing 15. The first statement uses tap to output the result of the promise. The second statement uses call to call the sayhi method and takes the return value of the method as the result of the promise. The third statement uses get to get the value of property b as a result of the promise. The fourth statement uses return to change the outcome of the promise. The fifth statement uses throw to throw the error and catch to handle the error. The sixth statement uses catchreturn to handle the error and change the value of the promise.

Listing 15Examples of the use of practical methods.

promise.resolve(1).tap(console.log).then(v => v + 1).then(console.log);promise.resolve().call('sayhi').then(console.log);promise.resolve().get('b').then(console.log);promise.resolve(1).return(2).then(console.log);promise.resolve(1).throw(new typeerror('type error')).catch(console.error);promise.reject(new typeerror('type error')).catchreturn('default value').then(console.log);
While it is possible to use the catch of promises to catch and handle errors, in many cases it is still possible that errors are not received due to problems with the program itself or third-party libraries. An uncaught error can cause the nodejs process to quit unexpectedly. Bluebird provides both global and local error handling mechanisms. Bluebird fires global events related to the rejection of the promise, which are unhandledrejection and rejectionhandled, respectively. You can add a global event handler to handle both types of events. For each promise, you can use the onpossiblyunhandledrejection method to add a default handling method for unhandled rejection errors. If you don't care about unhandled errors, you can use suppressUnhandledRejections to ignore errors.

In Listing 16, the rejection error of the promise is not handled and is handled by the global processor.

Listing 16Example of error handling.

promise.reject(new typeerror('error')).then(console.log);process.on('unhandledrejection', (reason, promise) => console.error(`unhandled $`
Promises are important abstractions related to asynchronous operations in j**ascript application development. As a popular promise library, Bluebird provides a lot of useful features related to promises. This article provides a detailed introduction to the important features of Bluebird, including how to create promise objects, use promises, work with collections, promise utilities, and error handling.

Related Pages