These five packages solve two distinct problems in JavaScript development: running shell commands or npm scripts from the command line, and managing asynchronous control flow within code. concurrently, npm-run-all, and yarn-run-all are CLI tools designed to be used in package.json to coordinate build steps or dev servers. async and p-queue are runtime libraries used inside JavaScript files to handle promises, callbacks, and concurrency limits. Understanding the difference between CLI script runners and in-code async utilities is key to picking the right tool.
Developers often need to run multiple tasks at once, whether it's starting servers during development or managing data fetches in code. The packages async, concurrently, npm-run-all, p-queue, and yarn-run-all address these needs but operate in different layers of your stack. Some are command-line tools for your package.json, while others are libraries for your JavaScript logic. Let's break down how they work and when to use each.
concurrently, npm-run-all, and yarn-run-all are tools you run from the terminal or define in package.json. They help you chain or parallelize build steps.
concurrently focuses on running multiple shell commands at the same time. It is ideal for development servers.
// package.json example for concurrently
{
"scripts": {
"dev": "concurrently \"npm run server\" \"npm run client\""
}
}
npm-run-all focuses on running npm scripts sequentially or in parallel using run-s and run-p. It is ideal for build pipelines.
// package.json example for npm-run-all
{
"scripts": {
"build": "run-s clean compile bundle"
}
}
yarn-run-all works similarly to npm-run-all but was originally targeted at yarn users. It uses the same run-s and run-p commands.
// package.json example for yarn-run-all
{
"scripts": {
"test": "run-s lint test:unit"
}
}
async and p-queue are libraries you import into your JavaScript files. They handle promises and callbacks during execution.
async provides a wide range of control flow functions like series, parallel, and waterfall. It supports both callbacks and promises.
// async example: running tasks in series
import async from 'async';
async.series([
(cb) => { console.log('Task 1'); cb(null, 1); },
(cb) => { console.log('Task 2'); cb(null, 2); }
], (err, results) => {
console.log(results); // [1, 2]
});
p-queue provides a promise queue with concurrency limits. It ensures only a set number of promises run at once.
// p-queue example: limiting concurrency
import PQueue from 'p-queue';
const queue = new PQueue({ concurrency: 2 });
queue.add(() => fetch('/api/1'));
queue.add(() => fetch('/api/2'));
queue.add(() => fetch('/api/3')); // Waits for a slot
Concurrency handling differs greatly between CLI tools and runtime libraries. CLI tools often run everything at once, while runtime libraries may need limits.
concurrently runs all commands in parallel with no built-in limit. If you start 10 processes, all 10 run.
# concurrently: runs all commands immediately
concurrently "node task1.js" "node task2.js" "node task3.js"
npm-run-all runs scripts in parallel with run-p, also without a concurrency limit.
# npm-run-all: runs all scripts immediately
npm-run-all --parallel build:css build:js build:assets
yarn-run-all behaves like npm-run-all, running all matched scripts in parallel.
# yarn-run-all: runs all scripts immediately
yarn-run-all --parallel test:unit test:e2e
async allows parallel execution but lets you set a limit using parallelLimit.
// async: parallel with limit
async.parallelLimit(tasks, 2, (err, results) => {
// Only 2 tasks run at once
});
p-queue is built specifically for limiting concurrency. You define the limit when creating the queue.
// p-queue: strict concurrency limit
const queue = new PQueue({ concurrency: 5 });
// Only 5 promises active at any time
How failures are handled determines if your build fails fast or continues.
concurrently can be configured to kill other processes if one fails using --kill-others-on-fail.
# concurrently: stop all if one fails
concurrently --kill-others-on-fail "npm run api" "npm run web"
npm-run-all stops immediately if a script fails in sequential mode. In parallel, it reports errors but exits with code 1.
# npm-run-all: stops on first error in sequence
run-s clean build bundle
yarn-run-all mirrors npm-run-all behavior, stopping on error in sequential mode.
# yarn-run-all: stops on first error
run-s lint test
async stops the series if a task passes an error to the callback, but parallel collects all errors.
// async: series stops on error
async.series([task1, task2], (err) => {
if (err) return; // Stops if task1 fails
});
p-queue throws an error if a promise rejects, unless you catch it inside the added function.
// p-queue: error handling
try {
await queue.add(() => riskyOperation());
} catch (error) {
console.error('Task failed');
}
Not all packages are equally maintained or necessary in modern workflows.
concurrently is actively maintained and widely used for local development setups.
// Status: Active
"devDependencies": {
"concurrently": "^8.0.0"
}
npm-run-all is stable and works with both npm and yarn, making it a safe choice for script composition.
// Status: Stable
"devDependencies": {
"npm-run-all": "^4.1.5"
}
yarn-run-all is largely redundant since npm-run-all supports yarn. It receives less attention.
// Status: Redundant
// Recommendation: Use npm-run-all instead
async is a legacy powerhouse. It is stable but native Promises often replace it in new code.
// Status: Stable (Legacy)
// Modern alternative: Promise.all()
p-queue is modern and actively maintained for promise-based concurrency needs.
// Status: Active
"dependencies": {
"p-queue": "^7.0.0"
}
| Feature | concurrently | npm-run-all | yarn-run-all | async | p-queue |
|---|---|---|---|---|---|
| Type | CLI Tool | CLI Tool | CLI Tool | Library | Library |
| Context | package.json | package.json | package.json | JavaScript Code | JavaScript Code |
| Concurrency | Unlimited Parallel | Unlimited Parallel | Unlimited Parallel | Configurable Limit | Configurable Limit |
| Primary Use | Dev Servers | Script Pipelines | Script Pipelines | Control Flow | Rate Limiting |
| Status | β Active | β Stable | β οΈ Redundant | β Stable | β Active |
concurrently is your go-to for local development π₯οΈ. Use it when you need to watch multiple processes like a backend API and a frontend bundler in one terminal.
npm-run-all is the standard for build scripts ποΈ. Use it to chain tasks like linting, testing, and building in a specific order within your CI/CD pipeline.
yarn-run-all is generally unnecessary π«. Since npm-run-all works with yarn, adding this package adds weight without benefit.
async is for complex legacy logic π°οΈ. Use it if you are maintaining older Node.js codebases with heavy callback usage, but prefer native Promises for new projects.
p-queue is for controlled concurrency π. Use it when you need to respect rate limits or prevent overwhelming a server with too many simultaneous requests.
Final Thought: Separate your concerns. Use CLI tools (concurrently, npm-run-all) for your package.json scripts, and use runtime libraries (p-queue, async) for logic inside your application code. This keeps your build configuration clean and your application logic focused.
Choose async if you are maintaining legacy code with callbacks or need complex control flow patterns like waterfalls, series, or parallel execution with rich error handling. It is a mature library that bridges callback and promise styles, making it ideal for older Node.js projects or complex async logic that native Promise methods don't cover cleanly.
Choose concurrently if you need to run multiple shell commands at the same time, such as starting a frontend dev server and a backend API simultaneously. It is the best fit for local development workflows where you want to see output from all processes in one terminal window with color coding.
Choose npm-run-all if you need to compose npm scripts sequentially or in parallel within your package.json. It provides run-s and run-p commands that make script dependencies clear and are compatible with both npm and yarn package managers.
Choose p-queue if you need to limit the number of promises running at once, such as respecting API rate limits or preventing browser overload. It is a lightweight, promise-based queue that gives you precise control over concurrency without the overhead of a full control flow library.
Avoid yarn-run-all for new projects. It is a fork of npm-run-all that was created when npm and yarn scripts behaved differently, but npm-run-all now works reliably with yarn. Using npm-run-all reduces dependencies and ensures better long-term maintenance.

Async is a utility module which provides straight-forward, powerful functions for working with asynchronous JavaScript. Although originally designed for use with Node.js and installable via npm i async, it can also be used directly in the browser. An ESM/MJS version is included in the main async package that should automatically be used with compatible bundlers such as Webpack and Rollup.
A pure ESM version of Async is available as async-es.
For Documentation, visit https://caolan.github.io/async/
For Async v1.5.x documentation, go HERE
// for use with Node-style callbacks...
var async = require("async");
var obj = {dev: "/dev.json", test: "/test.json", prod: "/prod.json"};
var configs = {};
async.forEachOf(obj, (value, key, callback) => {
fs.readFile(__dirname + value, "utf8", (err, data) => {
if (err) return callback(err);
try {
configs[key] = JSON.parse(data);
} catch (e) {
return callback(e);
}
callback();
});
}, err => {
if (err) console.error(err.message);
// configs is now a map of JSON data
doSomethingWith(configs);
});
var async = require("async");
// ...or ES2017 async functions
async.mapLimit(urls, 5, async function(url) {
const response = await fetch(url)
return response.body
}, (err, results) => {
if (err) throw err
// results is now an array of the response bodies
console.log(results)
})