What Really Happened with the is-promise NPM Incident? A Deep Dive

The article investigates the April 25 NPM outage caused by the is-promise package, explains the buggy code and the problematic "type" and "exports" fields in package.json, reviews the rapid fix timeline, and offers practical lessons for developers to avoid similar dependency mishaps.

WeDoctor Frontend Technology
WeDoctor Frontend Technology
WeDoctor Frontend Technology
What Really Happened with the is-promise NPM Incident? A Deep Dive
Qi Yunlei, front‑end engineer at WeDoctor cloud services team, focuses on Node.js ecosystem construction and web‑app solutions.

Preface

On April 25 the NPM community erupted over an update incident triggered by a package called is-promise.

Rumors claimed that a single‑line package had impacted major projects from Google, Facebook, Amazon and caused chaos across the JavaScript ecosystem.

However, the author did not feel any real disruption and wondered whether such a small, rarely used package could truly be that destructive.

Motivated by curiosity and skepticism, the author decided to investigate.

is-promise Overview

First, let’s look at the complete source code of is-promise version 2.1.0 before the incident.

module.exports = isPromise;

function isPromise(obj) {
  return !!obj && (typeof obj === 'object' || typeof obj === 'function') && typeof obj.then === 'function';
}

This is a fairly permissive "Promise‑like" checker; despite the name, it behaves more like an is‑thenable utility. Writing such concise logic correctly requires solid expertise.

For example, the preceding typeof check can filter out malformed thenable strings like String.prototype.then = function () {}.

We should not dismiss the package’s value. The Promise/A+ spec is a flexible standard, not a language feature, and many implementations adopt an inclusive check like this.

Similar packages, such as Sindre Sorhus’s p-is-promise, also add a catch method check.

Review

Let’s revisit the timeline of the incident.

Timeline

Author Forbes Lindesay summarized the main events:

2020‑04‑25T15:03:25Z — Published problematic 2.2.0

2020‑04‑25T17:16:00Z — Ryan Zimmerman submitted a fix PR

2020‑04‑25T17:48:00Z — Alert received on social media

2020‑04‑25T17:54:00Z — Merged Ryan’s PR, released 2.2.1

2020‑04‑25T17:57:00Z — Closed related issues, opened a new thread for discussion

2020‑04‑25T18:06:00Z — Jordan Harband noted remaining "exports" field issue

2020‑04‑25T18:08:08Z — Removed "exports" from package.json, released 2.2.2

2020‑04‑25T19:20:00Z — Reverted 2.2.0 and 2.2.1

The author’s reaction was swift, but the delayed revert was still problematic.

Now we analyze each 2.2.x version.

2.2.0

Added TypeScript declaration file

Supported ES‑module style import

From a “god‑view” we see the root cause: two new fields were added to package.json:

{
  "type": "module",
  "exports": {
    "import": "index.mjs",
    "require": "index.js"
  }
}

Two types of errors quickly surfaced.

Error 1: the "exports" file paths omitted the required "./" prefix, causing Node.js to throw:

Error [ERR_INVALID_PACKAGE_TARGET]: Invalid "exports" main target "index.js" defined in the package config ...; targets must start with "./"

Error 2: setting type: "module" disabled require, forcing imports to use ES modules:

Error [ERR_REQUIRE_ESM]: Must use import to load ES Module: .../is-promise/index.js

A hidden third error was the missing update of the files field, so index.mjs and index.d.ts were not packaged.

2.2.1

Fixed the incorrect ESM usage

The corrected package.json looks like:

{
  "exports": {
    "import": "./index.mjs",
    "require": "./index.js"
  }
}

However, requiring other files via require('is-promise/package.json') now throws:

Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath './package.json' is not defined by "exports"

Even imports like require('is-promise/index') are blocked.

2.2.2

Removed the exports field from package.json This eliminated the breaking change introduced in 2.2.0.

Problematic Fields Explained

The incident stemmed from two relatively obscure package.json fields: type and exports.

type

The type field determines which module system the files in the directory follow. It can be:

commonjs : .js and .cjs follow CommonJS, .mjs follows ESM.

module : .js and .mjs follow ESM, .cjs follows CommonJS.

In early Node.js 12 versions the feature required the --experimental-modules flag; it became stable in Node 13.2.0. The author set type: "module", which broke CommonJS users.

exports

The exports field, introduced as the proposal-pkg-exports, allows fine‑grained control over which files are exposed to different environments. It supplements the traditional main field.

When exports is defined, it overrides main and only the explicitly listed files can be imported.

{
  "main": "./main.js",
  "exports": {
    ".": "./main.js",
    "./feature": {
      "browser": "./feature-browser.js",
      "default": "./feature.js"
    }
  }
}

Node.js versions that support exports require the paths to start with "./"; omitting it leads to the earlier error.

Author’s Post‑Mortem

The author later published a post‑mortem outlining four main causes:

Local publishing without CI verification

Using new features without CI supporting the required Node version

Testing only the code, not the published package

Author unavailable, other maintainers lacked a way to publish a fix

In short: insufficient testing and an unsystematic release process.

Impact Assessment

Issues were found in projects like create‑react‑app, @angular/cli, and firebase‑tools, causing install or build failures.

The is-promise package has tens of millions of weekly downloads, with 766 direct dependents (now 561 after the incident) and over 3.5 million GitHub dependents.

The bug existed for roughly three hours, but NPM’s caching extended the real‑world impact.

While the reach was broad, the actual damage was not as dramatic as some rumors suggested.

Observer’s Reflections

How can we prevent similar tragedies?

Pin Versions

Locking dependencies eliminates this class of surprise, especially for application developers.

However, library authors should avoid over‑locking to keep receiving upstream fixes.

Unit Tests

Testing is essential. The is-promise change lacked any test coverage, even for basic require usage.

Small Libraries

Even tiny libraries carry maintenance overhead; developers should evaluate whether a dependency is truly needed.

Documentation

The type field’s impact was clear, but the exports field’s breaking nature was poorly documented, leading the author to underestimate its risk.

Package Managers

Both Yarn and NPM have converged in features; mixing them is discouraged.

Yarn’s speed advantage is no longer significant.

Tools like PNPM that aim to improve the NPM ecosystem are worth watching.

Conclusion

Critics of the NPM ecosystem often do not contribute to its improvement; constructive voices are rare.

Users need not panic—human error is inevitable, but disciplined processes can greatly reduce negative impact.

Sometimes harsh advice is valuable; we hope more useful perspectives emerge.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

dependency managementpackage.jsonExportsnodejsnpmis-promisetype field
WeDoctor Frontend Technology
Written by

WeDoctor Frontend Technology

Official WeDoctor Group frontend public account, sharing original tech articles, events, job postings, and occasional daily updates from our tech team.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.