ES6 promise: patterns and antipatterns

Time:2020-11-26

ES6 promise: patterns and anti patterns
By Bob Brennan

When nodejs was first used a few years ago, I was troubled by what is now known as “callback hell.”. Fortunately, it’s 2017 now, nodejs has adopted a lot of the latest features of JavaScript, and has supported promise since v4.

While promise can make code simpler and easier to read, for those who are only familiar with callback functions, this may be questionable. Here, I’ll list some of the basic patterns I learned when using promise, as well as some of the pits I stepped on.

be careful:In this paperArrow functions will be used. If you are not familiar with them, they are very simple. It is recommended to read the advantages of using them

Models and best practices

Using promise

If you are using a third-party library that already supports promise, it is very easy to use. Just care about two functions:then()andcatch()。 For example, there is a client API that contains three methods,getItem()updateItem(), anddeleteItem()Each method returns a promise:

Promise.resolve()
  .then(_ => {
    return api.getItem(1)
  })
  .then(item => {
    item.amount++
    return api.updateItem(1, item);
  })
  .then(update => {
    return api.deleteItem(1);
  })
  .catch(e => {
    console.log('error while working on item 1');
  })

Every callthen()A new step is created in the promise chain, and if an error occurs anywhere in the chain, the next step is triggeredcatch()then()andcatch()Can return a value or a new promise, and the result will be passed to the next in the promise chainthen()

For comparison, the callback function is used to implement the same logic:

api.getItem(1, (err, data) => {
  if (err) throw err;
  item.amount++;
  api.updateItem(1, item, (err, update) => {
    if (err) throw err;
    api.deleteItem(1, (err) => {
      if (err) throw err;
    })
  })
})

The first difference to note is that, using callback functions, we have toeachStep, instead of a single catch all. The second problem with callback functions is more intuitive. Each step should be indented horizontally, while the code using promise has an obvious sequence relationship.

Promise callback function

The first skill to learn is how to convert callback functions to promise. You may be using a library that is still based on callbacks, or your own old code, but don’t worry, it takes only a few lines of code to wrap it into a promise. This is a callback method in nodefs.readFileExample of conversion to promise:

function readFilePromise(filename) {
  return new Promise((resolve, reject) => {
    fs.readFile(filename, 'utf8', (err, data) => {
      if (err) reject(err);
      else resolve(data);
    })
  })
}

readFilePromise('index.html')
  .then(data => console.log(data))
  .catch(e => console.log(e))

The key part is the promise constructor, which takes a function as an argument. This function has two function parameters:resolveandreject。 All the work is done in this function. After that, it is called on successresolveCall if there is an errorreject

It should be noted that onlyOneresolveperhapsrejectCalled, that is, it should be called only once. In our example, iffs.readFileReturns an error, which we pass torejectOtherwise, the file data is passed to theresolve

Value of promise

ES6 has two very convenient auxiliary functions for creating promise with normal valuesPromise.resolve()andPromise.reject()。 For example, you may need a function that returns promise when you synchronize certain situations:

function readFilePromise(filename) {
  if (!filename) {
    return Promise.reject(new Error("Filename not specified"));
  }
  if (filename === 'index.html') {
    return Promise.resolve('<h1>Hello!</h1>');
  }
  return new Promise((resolve, reject) => {/*...*/})
}

Note that although you can pass anything (or no value) to thePromise.reject()But it’s a good idea to pass on oneError

Run in parallel

Promise.allIs a method that runs promise arrays in parallel, that is, at the same time. For example, we have a list of files to read from disk. Use thereadFilePromiseFunction, which will look like this:

let filenames = ['index.html', 'blog.html', 'terms.html'];

Promise.all(filenames.map(readFilePromise))
  .then(files => {
    console.log('index:', files[0]);
    console.log('blog:', files[1]);
    console.log('terms:', files[2]);
  })

I don’t even try to write the equivalent code using the traditional callback function, which is messy and error prone.

Serial operation

Sometimes running a bunch of promises at the same time can cause problems. For example, if you try to usePromise.allWhen the rate limit is reached, it may start to respond to 429 errors.

One solution is to run promise serially, or one after another. But no similar is provided in ES6Promise.allThis approach (why?) But we can use itArray.reduceTo achieve:

let itemIDs = [1, 2, 3, 4, 5];

itemIDs.reduce((promise, itemID) => {
  return promise.then(_ => api.deleteItem(itemID));
}, Promise.resolve());

In this case, we need to wait for each callapi.deleteItem()You can’t make the next call until it’s done. This method is better than writing for each itemid.then()More concise and versatile:

Promise.resolve()
  .then(_ => api.deleteItem(1))
  .then(_ => api.deleteItem(2))
  .then(_ => api.deleteItem(3))
  .then(_ => api.deleteItem(4))
  .then(_ => api.deleteItem(5));

Race

Another convenient function provided by ES6 isPromise.race。 FollowPromise.allSimilarly, receive a promise array and run them at the same time, but the difference is that once thewhateverReturn if promise completes or fails and discards all other results.

For example, we can create a promise that times out after a few seconds:

function timeout(ms) {
  return new Promise((resolve, reject) => {
    setTimeout(reject, ms);
  })
}

Promise.race([readFilePromise('index.html'), timeout(1000)])
  .then(data => console.log(data))
  .catch(e => console.log("Timed out after 1 second"))

It’s important to note that the other promise will continue to run, but you won’t see the results.

Capture error

The most common way to catch errors is to add a.catch()Code block, which will capture all previous.then()Error in code block:

Promise.resolve()
  .then(_ => api.getItem(1))
  .then(item => {
    item.amount++;
    return api.updateItem(1, item);
  })
  .catch(e => {
    console.log('failed to get or update item');
  })

Here, as long as there isgetItemperhapsupdateItemFailure,catch()It will be triggered. But if we want to deal with it separatelygetItemWhat to do with your mistakes? Just insert another onecatch()Yes, it can also return another promise.

Promise.resolve()
  .then(_ => api.getItem(1))
  .catch(e => api.createItem(1, {amount: 0}))
  .then(item => {
    item.amount++;
    return api.updateItem(1, item);
  })
  .catch(e => {
    console.log('failed to update item');
  })

Now, ifgetItem()Failure, we pass the first onecatchStep in and create a new record.

Throw an error

Should bethen()All code in the statement is treated astryBlock.return Promise.reject()andthrow new Error()Will lead to the nextcatch()The running of the code block.

This means that runtime errors are also triggeredcatch()So don’t assume the source of the error. For example, in the following code, we might want thecatch()Can only getgetItemBut as the example shows, it will also be in ourthen()Statement to catch a runtime error.

api.getItem(1)
  .then(item => {
    delete item.owner;
    console.log(item.owner.name);
  })
  .catch(e => {
    console.log(e); // Cannot read property 'name' of undefined
  })

Dynamic chain

Sometimes we want to build promise chains dynamically, for example, by inserting an extra step when certain conditions are met. In the following example, we can choose to create a lock file before reading a given file:

function readFileAndMaybeLock(filename, createLockFile) {
  let promise = Promise.resolve();

  if (createLockFile) {
    promise = promise.then(_ => writeFilePromise(filename + '.lock', ''))
  }

  return promise.then(_ => readFilePromise(filename));
}

It has to be rewrittenpromise = promise.then(/*...*/)To updatePromiseValue of. See the next section on antipatternsCall then() more than once

Antipattern

Promise is a neat abstraction, but it’s easy to fall into some pitfalls. Here are some of the most common problems I encounter.

Back to hell

When I first switched from callback functions to promise, I found it difficult to get rid of some old habits and still nest promise like using callback functions

api.getItem(1)
  .then(item => {
    item.amount++;
    api.updateItem(1, item)
      .then(update => {
        api.deleteItem(1)
          .then(deletion => {
            console.log('done!');
          })
      })
  })

This nesting is completely unnecessary. Sometimes one or two levels of nesting can help compose related tasks, but it’s best to always use.then()Rewrite as promise vertical chain.

No return

One of the common mistakes I make is forgetting in a promise chainreturnsentence. Can you find the following bugs?

api.getItem(1)
  .then(item => {
    item.amount++;
    api.updateItem(1, item);
  })
  .then(update => {
    return api.deleteItem(1);
  })
  .then(deletion => {
    console.log('done!');
  })

Because we don’t have one on line 4api.updateItem()Write it in front of youreturn, sothen()The code block will immediately resolve, causingapi.deleteItem()Maybe in the api.updateItem()Called before it is finished.

In my opinion, this is a big problem with ES6 promise, which often leads to unexpected behavior. The problem is,.then()You can also return a new value of promise,undefinedIs a valid return value. Personally, if I was in charge of the promise API, I would.then()returnundefinedThrow a runtime error, but now we need to pay special attentionreturnPromise created.

Multiple calls.then()

According to the specification, call multiple times on the same promisethen()Is fully valid, and the callback will be called in the order in which it was registered. However, I haven’t seen the unexpected behavior and return value when I do this:

let p = Promise.resolve('a');
p.then(_ => 'b');
p.then(result => {
  console.log(result) // 'a'
})

let q = Promise.resolve('a');
q = q.then(_ => 'b');
q = q.then(result => {
  console.log(result) // 'b'
})

In this case, because we’re callingthen()Do not updatepSo we can’t see it'b'return. But every time you callthen()Update whenqSo their behavior is more predictable.

This also applies to error handling:

let p = Promise.resolve();
p.then(_ => {throw new Error("whoops!")})
p.then(_ => {
  console.log('hello!'); // 'hello!'
})

let q = Promise.resolve();
q = q.then(_ => {throw new Error("whoops!")})
q = q.then(_ => {
  console.log('hello'); // We never reach here
})

Here, we expect to throw an error to break the promise chain, but there is no updatepSo the secondthen()Will still be called.

It is possible to call multiple times on a promise.then()There are many reasons, because it allows promise to be assigned to several new independent promises, but no real usage scenarios have been found.

Mix callback and promise

It’s easy to fall into the trap of working in a callback based project while using the promise library. Always avoidthen()orcatch()Use the callback function   otherwise promise will devour any subsequent errors as part of the promise chain. For example, it seems reasonable to use callback functions to wrap a promise:

function getThing(callback) {
  api.getItem(1)
    .then(item => callback(null, item))
    .catch(e => callback(e));
}

getThing(function(err, thing) {
  if (err) throw err;
  console.log(thing);
})

The problem here is that if there is an error, we get a warning about “unhandled project rejection,” even if we add onecatch()Code block. This is because,callback()staythen()andcatch()Will be called to make it part of the promise chain.

If you have to wrap promise with a callback, you can use thesetTimeout(or in nodejsprocess.nextTick)To break promise:

function getThing(callback) {
  api.getItem(1)
    .then(item => setTimeout(_ => callback(null, item)))
    .catch(e => setTimeout(_ => callback(e)));
}

getThing(function(err, thing) {
  if (err) throw err;
  console.log(thing);
})

Do not catch errors

Error handling in JavaScript is a bit strange. Although support familiartry/catchHowever, there is no way to force the caller to handle errors in a Java way. However, it becomes common to use callback functions, called “errbacks,” where the first parameter is an error callback. This forces the caller to at least acknowledge the possibility of error. For example,fsLibrary:

fs.readFile('index.html', 'utf8', (err, data) => {
  if (err) throw err;
  console.log(data);
})

With promise, it’s easy to forget the need for error handling, especially for sensitive operations such as file system and database access. At present, if you do not capture the project of reject, you will see a very ugly warning in nodejs:

(node:29916) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: whoops!
(node:29916) DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

Be sure to add at the end of any promise chain in the main event loopcatch()To avoid this.

summary

Hopefully, this is a useful overview of common promise patterns and antipatterns. If you want to learn more, here are some useful resources:

  • Mozilla’s ES6 promise documentation
  • Promise from Google
  • Overview of David Atchley’s ES6 promise

More promise patterns and antipatterns

Or read from the datafire team

Recommended Today

The course of using Chinese software of poedit Pro

Poedit pro (formerly known as poedit) is a free (professional version charge), open source and cross platform gettext class (. Po format, gettext is used in the application program for program internationalization) International Translation editor. It is also one of the most widely used software of the same type. At present, it is available in […]