14 Linting Rules To Help You Write Asynchronous Code in JavaScript
Debugging asynchronous code in JavaScript can feel like navigating a minefield at times. You don't know when and where the console.logs will print out, and you have no idea how your code is executed.
It's hard to correctly structure async code so it executes in the right order as you intend it to.
Wouldn't it be nice if you had some guidance while writing asynchronous code, and to get a helpful message when you're about to make a mistake?
Luckily we have linters to catch some of our bugs before we push them to production. The following is a compiled list of linting rules to specifically help you with writing asynchronous code in JavaScript and Node.js.
Even if you end up not using the rules in your project, reading their descriptions will lead to a better understanding of async code and improve your developer skills.
ESLint rules for asynchronous code
The following rules are shipped by default withESLint. Enable them by adding them to your eslint configuration file.
1. no-async-promise-executor
This rule disallows passing anasync
function to thenew Promise
constructor.
// ❌newPromise(async (resolve,reject)=> {});// ✅newPromise((resolve,reject)=> {});
While it's technically valid to pass an asynchronous function to the Promise constructor, doing so is usually a mistake for any of these two reasons. First, if the async function throws, the error will be lost and won't be rejected by the newly-constructed promise. Second, ifawait
is used inside the constructor function, the wrapping Promise might be unnecessary and you could remove it.
2. no-await-in-loop
This rule disallows usingawait
inside loops.
When doing an operation on each element of an iterable and awaiting an asynchronous task, it's oftentimes an indication that the program is not taking full advantage of JavaScript's event-driven architecture. Byexecuting the tasks in parallel instead, you could greatly improve the efficiency of your code.
// ❌for (consturlofurls) {constresponse=awaitfetch(url);}// ✅constresponses= [];for (consturlofurls) {constresponse=fetch(url);responses.push(response);}awaitPromise.all(responses);
In the odd case when you deliberately want to run tasks in sequence, I recommend temporarily disabling this rule with an inline comment:// eslint-disable-line no-await-in-loop
.
3. no-promise-executor-return
This rule disallows returning a value inside a Promise constructor.
// ❌newPromise((resolve,reject)=> {returnresult;});// ✅newPromise((resolve,reject)=> {resolve(result);});
Values returned inside a Promise constructor cannot be used and don't affect the promise in any way. The value should be passed toresolve
instead, or if an error occurred, callreject
with the error.
This rule won't prevent you from returning a value inside a nested callback within the Promise constructor. Always make sure to useresolve
orreject
to conclude a promise.
4. require-atomic-updates
This rule disallows assignments in combination withawait
, which can lead to race conditions.
Consider the example below, what do you think the final value oftotalPosts
will be?
// ❌lettotalPosts=0;asyncfunctiongetPosts(userId) {constusers= [{id:1,posts:5 }, {id:2,posts:3 }];awaitsleep(Math.random()*1000);returnusers.find((user)=>user.id===userId).posts;}asyncfunctionaddPosts(userId) {totalPosts+=awaitgetPosts(userId);}awaitPromise.all([addPosts(1),addPosts(2)]);console.log('Post count:',totalPosts);
Perhaps you've sensed that this was a trick question and the answer is not 8. That's right,totalPosts
prints either 5 or 3. Go ahead and try it yourself in the browser.
The issue lies in the fact that there is a time gap between reading and updatingtotalPosts
. This causes a race condition such that when the value is updated in a separate function call, the update is not reflected in the current function scope. Therefore both functions add their result tototalPosts
's initial value of 0.
To avoid this race condition you should make sure the variable is read at the same time it's updated.
// ✅lettotalPosts=0;asyncfunctiongetPosts(userId) {constusers= [{id:1,posts:5 }, {id:2,posts:3 }];awaitsleep(Math.random()*1000);returnusers.find((user)=>user.id===userId).posts;}asyncfunctionaddPosts(userId) {constposts=awaitgetPosts(userId);totalPosts+=posts;// variable is read and immediately updated}awaitPromise.all([addPosts(1),addPosts(2)]);console.log('Post count:',totalPosts);
5. max-nested-callbacks
This rule enforces a maximum nesting depth for callbacks. In other words, this rule prevents callback hell!
/* eslint max-nested-callbacks: ["error", 3] */// ❌async1((err,result1)=> {async2(result1, (err,result2)=> {async3(result2, (err,result3)=> {async4(result3, (err,result4)=> {console.log(result4); }); }); });});// ✅constresult1=awaitasyncPromise1();constresult2=awaitasyncPromise2(result1);constresult3=awaitasyncPromise3(result2);constresult4=awaitasyncPromise4(result3);console.log(result4);
A deep level of nesting makes code hard to read and more difficult to maintain.Refactor callbacks to promises and use modern async/await syntax when writing asynchronous code JavaScript.
6. no-return-await
This rule disallows unnecessaryreturn await
.
// ❌async ()=> {returnawaitgetUser(userId);}// ✅async ()=> {returngetUser(userId);}
Awaiting a promise and immediately returning it is unnecessary sinceall values returned from anasync
function are wrapped in a promise. Therefore you can return the promise directly.
An exception to this rule is when there is a surroundingtry...catch
statement. Removing theawait
keyword will cause a promise rejection to not be caught. In this case, I suggest you assign the result to a variable on a different line to make the intent clear.
// 👎async ()=> {try {returnawaitgetUser(userId); }catch (error) {// Handle getUser error }}// 👍async ()=> {try {constuser=awaitgetUser(userId);returnuser; }catch (error) {// Handle getUser error }}
7. prefer-promise-reject-errors
This rule enforces using anError
object when rejecting a Promise.
// ❌Promise.reject('An error occurred');// ✅Promise.reject(newError('An error occurred'));
It's best practice to always reject a Promise with anError
object. Doing so will make it easier to trace where the error came from because error objects store a stack trace.
Node.js specific rules
The following rules are additional ESLint rules for Node.js provided by theeslint-plugin-n
plugin. To use them, you need to install and add the plugin to theplugins
array in your eslint configuration file.
8. node/handle-callback-err
This rule enforces error handling inside callbacks.
// ❌functioncallback(err,data) {console.log(data);}// ✅functioncallback(err,data) {if (err) {console.log(err);return; }console.log(data);}
In Node.js it's common to pass the error as the first parameter to a callback function. Forgetting to handle the error can result in your application behaving strangely.
This rule is triggered when the first argument of a function is namederr
. In large projects, it's not uncommon to find different naming variations for errors such ase
orerror
. You can change the default configuration by providing a second argument to the rule:node/handle-callback-err: ["error", "^(e|err|error)$"]
9. node/no-callback-literal
This rule enforces that a callback function is called with anError
object as the first parameter. In case there's no error,null
orundefined
are accepted as well.
// ❌cb('An error!');callback(result);// ✅cb(newError('An error!'));callback(null,result);
This rule makes sure you don't accidentally invoke a callback function with a non-error as the first parameter. According to theerror-first callback convention, the first argument of a callback function should be the error or otherwise null or undefined if there's no error.
The rule is triggered only when the function is namedcb
orcallback
.
10. node/no-sync
This rule disallows using synchronous methods from the Node.js core API where an asynchronous alternative exists.
// ❌constfile=fs.readFileSync(path);// ✅constfile=awaitfs.readFile(path);
Using synchronous methods for I/O operations in Node.jsblocks the event loop. In most web applications, you want to use asynchronous methods when doing I/O operations.
In some applications like a CLI utility or a script, using the synchronous method is okay. You can disable this rule at the top of the file with/* eslint-disable node/no-sync */
.
Additional rules for TypeScript users
If your project is using TypeScript you're probably familiar withTypeScript ESLint (previously TSLint). The following rules are available to TypeScript projects only because they infer extra context from typing information.
11. @typescript-eslint/await-thenable
This rule disallows awaiting a function or value that is not a Promise.
// ❌functiongetValue() {returnsomeValue;}awaitgetValue();// ✅asyncfunctiongetValue() {returnsomeValue;}awaitgetValue();
While it's valid JavaScript to await a non-Promise value (it will resolve immediately), it's often an indication of a programmer error, such as forgetting to add parentheses to call a function that returns a Promise.
12. @typescript-eslint/no-floating-promises
This rule enforces Promises to have an error handler attached.
// ❌myPromise() .then(()=> {});// ✅myPromise() .then(()=> {}) .catch(()=> {});
This rule prevents floating Promises in your codebase. A floating Promise is a Promise that doesn't have any code to handle potential errors.
Always make sure to handle promise rejections otherwise yourNode.js server will crash.
13. @typescript-eslint/no-misused-promises
This rule disallows passing a Promise to places that aren't designed to handle them, such as if-conditionals.
// ❌if (getUserFromDB()) {}// ✅ 👎if (awaitgetUserFromDB()) {}// ✅ 👍constuser=awaitgetUserFromDB();if (user) {}
This rule prevents you from forgetting to await an async function in a place where it would be easy to miss.
While the rule does allow awaiting inside an if-conditional, I recommend assigning the result to a variable and using the variable in the conditional for improved readability.
14. @typescript-eslint/promise-function-async
This rule enforces Promise-returning functions to beasync
.
// ❌functiondoSomething() {returnsomePromise;}// ✅asyncfunctiondoSomething() {returnsomePromise;}
A non-async function that returns a promise can be problematic since it can throw an Error objectand return a rejected promise. Code is usually not written to handle both scenarios. This rule makes sure a function returns a rejected promiseor throws an Error, but never both.
Additionally, it's much easier to navigate a codebase when youknow that all functions that return a Promise and are therefore asynchronous, are marked asasync
.
Enable these rules in your project right now
I've published anESLint configuration package that you can easily add to your project. It exports the base rules, Node.js specific rules, and TypeScript specific rules separately.
For non-Typescript users
Install the package and ESLint:
npminstall--save-deveslinteslint-config-async
Then in youreslint.config.js
configuration file add the following:
constasyncConfig=require("eslint-config-async");module.exports= [ ...asyncConfig.base,// enable base rules ...asyncConfig.node,// enable Node.js specific rules (recommended)];
For TypeScript users
Install the package and its peer dependencies:
npminstall--save-deveslinteslint-config-asynctypescripttypescript-eslint
In youreslint.config.js
configuration file:
consttseslint=require("typescript-eslint");constasyncConfig=require("eslint-config-async");module.exports= [tseslint.configs.base,// adds the parser only, without any rules ...asyncConfig.base,// enable base rules ...asyncConfig.node,// enable Node.js specific rules (recommended) ...asyncConfig.typescript,// enable TypeScript specific rules {files: ["*.ts"],// tell ESLint to include TypeScript fileslanguageOptions: {parserOptions: {projectService:true,tsconfigRootDir:__dirname, }, }, },];
That's it! Add these linting rules for asynchronous code to your project and fix any issues that show up. You might squash a bug or two! 🐛 🚫
Transform Callbacks into Clean Async Code! 🚀
Tired of messy callback code? Download thisFREE 5-step guide to master async/await and simplify your asynchronous code.
In just a few steps, you'll transform complex logic into readable,modern JavaScript that's easy to maintain. With clear visuals, each step breaks down the process so you can follow along effortlessly.

You'll also get tips on building scalable Node.js applications about twice a month. I respect your email privacy. Unsubscribe any time.