Open-source project
caolan/async avatar
caolan/async

caolan/async: callback-era control flow for JavaScript that still ships

Async utilities for node and the browser

28,120 stars2,382 forksJavaScriptMIT

At a glance

What is it?
caolan/async is a utility module of higher-order functions for asynchronous JavaScript, usable in Node.js and the browser. It is a mature, MIT-licensed package whose value now lies in collections, queues and callback plumbing rather than in promise syntax.
Who is it for?
Adopt caolan/async if you maintain callback-style Node code, need bounded concurrency over a collection, or want a queue with adjustable concurrency and no framework. Do not adopt it if your codebase is already fully promise-based and you only need sequential steps, because async/await covers that without a dependency.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 2 days ago.
What is it written in?
Mainly JavaScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 28, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The problem caolan/async solves, and who still has it

Coordinate many asynchronous operations without writing your own bookkeeping. The README describes async as "a utility module which provides straight-forward, powerful functions for working with asynchronous JavaScript," originally designed for Node.js and installable via npm i async. That framing is accurate and also tells you who it is for: code that passes Node-style callbacks, where each function takes a value, an index or key, and a callback.

The first README example reads three JSON files into an object. It calls async.forEachOf over {dev: "/dev.json", test: "/test.json", prod: "/prod.json"}, reads each file, parses it, and only then runs doSomethingWith(configs). The error path is explicit: if fs.readFile fails, the callback receives the error and the final callback logs err.message. If you have written that fan-out by hand, including the case where one file fails while two reads are still in flight, you already know why the module exists.

The second audience is concurrency control. async.mapLimit(urls, 5, async function(url) {...}) maps over urls with at most five operations running at once. That is a different job from Promise.all, which starts everything immediately. If you are hitting an API with a rate limit, mapLimit is the function you reach for.

How the module is built: lib/, dist/, and an ESM entry point

The repository layout separates source from shipped artifacts. Source lives in lib/, with lib/index.js as the index source named in the Makefile, and the build writes bundles into dist/. The package.json main field is dist/async.js, so a CommonJS require("async") resolves to the built UMD bundle rather than to lib/ directly.

The Makefile documents the build as a release-time job: "This build should be run once per release," and it expects dist/ artifacts to be checked in "so people on all platforms can run npm scripts." It produces a UMD bundle at build/dist/async.js, a minified UMD bundle at build/dist/async.min.js, and a module bundle at build/dist/async.mjs. Aliases are generated from support/aliases.txt, which means the individual function entry points you import are generated rather than hand-written.

The README states that an ESM/MJS version is included in the main async package and "should automatically be used with compatible bundlers such as Webpack and Rollup." A pure ESM version is published separately as async-es. That split matters in practice: if your bundler resolves the ESM field, you get tree-shakeable module code; if it does not, you get the UMD build. The README does not document what happens when both are present in one dependency graph.

Installing async and running a first forEachOf

Install from npm. The README gives the package name directly.

bash
npm i async

Then require it. The README's first example uses CommonJS and the forEachOf signature (value, key, callback), with a final callback that receives an error.

javascript
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);
    doSomethingWith(configs);
});

What you should see: configs is populated with parsed JSON keyed by dev, test and prod, and doSomethingWith runs once after every read settles. If any read fails, the final callback receives that error instead. Note the try/catch around JSON.parse. forEachOf does not catch exceptions thrown inside the iteratee for you, so a malformed file would otherwise escape the callback contract.

The second README example shows the promise-aware form, where the iteratee is an async function and the final argument is still a Node-style callback.

javascript
async.mapLimit(urls, 5, async function(url) {
    const response = await fetch(url)
    return response.body
}, (err, results) => {
    if (err) throw err
    console.log(results)
})

Here results is an array of response bodies in the order of urls, with no more than five fetches in flight. That ordering guarantee is the reason to use mapLimit over a hand-rolled worker pool.

Where caolan/async is the wrong tool

If your code is already written with async/await and you need three steps in sequence, adding this dependency buys you nothing. Native await handles sequencing, and Promise.all handles fan-out. The module earns its place when you need bounded concurrency, ordered results, or a queue, and those are the cases the README leads with.

The callback contract is the second boundary. Every iteratee must call its callback exactly once. Call it twice and the final callback can fire twice; never call it and the whole chain stalls with no error. The documentation does not describe a timeout or a watchdog for a forgotten callback, so a single early return inside an iteratee is enough to hang a request handler. This is the classic failure mode of callback-style control flow, and async does not remove it, it only organises it.

The third boundary is version drift. The published version in package.json is 3.2.6, while the README links a separate document for Async v1.5.x. The most recent releases listed for the repository are v2.3.0, v2.2.0 and v2.1.5, all dated 2017-04-06. Anyone copying a v1 or v2 snippet from a blog post is working from a different API surface than the current package, and the README does not provide a migration table.

async versus native promises and versus Bluebird

The honest comparison is not async against a rival utility library. It is async against the language. Native promises and async/await give you sequencing and Promise.all, and they need no dependency. What they do not give you is a named function for every concurrency shape. mapLimit with a limit argument, forEachOf over an object with both key and value, and a queue with adjustable concurrency are the gap async fills, and they are why the module has outlived the callback era it was written for.

A closer alternative is Bluebird, which appears in this repository's devDependencies alongside es6-promise, rsvp and native-promise-only. That placement is informative: the project benchmarks and tests against promise implementations rather than depending on one. Bluebird's approach is to replace the promise implementation itself, adding utilities on top of a Promise subclass. async's approach is to leave promises alone and supply standalone functions that accept either callbacks or promise-returning iteratees, as the mapLimit example shows. If you want one library that owns both the promise type and the helpers, Bluebird is the different design; if you want helpers that work with whatever promise your runtime already has, async is the one that does not care.

Maintenance, licence and upgrade cost

The repository is not archived, and the last push was on 2026-08-07. The release history is uneven: the recent releases listed are v2.3.0, v2.2.0 and v2.1.5, all dated 2017-04-06, while package.json carries version 3.2.6. Treat the npm version as the current one and the release list as incomplete rather than reading the 2017 dates as the state of the package.

Upgrade cost concentrates in one place: the major version boundary. The README points to a separate v1.5.x document, which is the project's own signal that v1 to v2 to v3 were breaking. There is a CHANGELOG.md at the repository root, and that is where a migration should start. The README itself does not document rollback or a deprecation policy for removed functions.

Licence is MIT, stated in the repository metadata and shipped as a LICENSE file at the top level. MIT permits commercial use and modification with the licence and copyright notice retained in distributions. That is the general shape of the terms, not legal advice; read LICENSE and your own counsel's guidance before relying on it for a distributed product.

One build detail affects packaged output. The Makefile expects dist/ artifacts to be checked in and describes the build as a once-per-release job, so consumers installing from npm receive prebuilt bundles rather than a compile step. It also says the Makefile is meant to be run on OSX/Linux, which is worth knowing if you intend to rebuild the bundles yourself on another platform.

Editorial conclusion

Adopt caolan/async if you maintain callback-style Node code, need bounded concurrency over a collection, or want a queue with adjustable concurrency and no framework. Do not adopt it if your codebase is already fully promise-based and you only need sequential steps, because async/await covers that without a dependency. Verify first that the functions you plan to call are still exported from the current package: the README points to https://caolan.github.io/async/ as the documentation, and the repository's docs/ directory is the source for it. Check the CHANGELOG.md before upgrading a major version, because the README links separate documentation for v1.5.x.

Frequently asked questions

How do I install caolan/async?

The README gives the command npm i async for Node.js. It also states the module can be used directly in the browser, and that an ESM/MJS version is included in the main async package for compatible bundlers such as Webpack and Rollup.

What is caolan/async used for?

It is a utility module providing functions for working with asynchronous JavaScript, originally designed for Node.js. The README examples cover reading several files into one object with forEachOf and fetching URLs with a concurrency limit through mapLimit.

Does caolan/async work with async/await?

Yes. The README's second example passes an async function as the iteratee to async.mapLimit and still uses a final Node-style callback to receive the results or an error.

Official sources

  1. caolan/async on GitHub
  2. License: MIT
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/caolan-async.svg)](https://hysenlabs.com/projects/caolan-async)