On April 25, 2020, a single-line change in is-promise version 2.2.0 disrupted build pipelines across 12,000,000 weekly downloads. The maintainer fixed it three hours later, but the incident highlighted a critical misunderstanding: semantic versioning is a contract, not a style preference.
This guide provides a framework to prevent similar issues in your packages and protect your organization from upstream dependency failures.
Guide Scope
This guide covers:
- Implementing semantic versioning as a system for detecting breaking changes
- End-to-end testing strategies for package releases
- Dependency pinning strategies that balance security and stability
- Incident response when dependencies fail unexpectedly
This guide does not cover supply-chain attacks or malicious code injection. The is-promise incident was a configuration error, not a security compromise.
Key Concepts and Definitions
Semantic Versioning (SemVer): Version format MAJOR.MINOR.PATCH where:
- MAJOR = breaking changes (incompatible API changes)
- MINOR = new functionality, backward compatible
- PATCH = backward-compatible bug fixes
Breaking Change: Any modification requiring downstream users to change their code, such as:
- Removing exported functions or properties
- Changing module export mechanisms (CommonJS vs ES modules)
- Altering function signatures
- Modifying default behaviors that code depends on
Dependency Depth: is-promise had 500 direct dependencies, each with its own dependents, creating potential for cascade failures.
Requirements Breakdown
For Package Maintainers
1. Version Bumps Must Match Change Scope
If you modify the exports field in package.json, you change how consumers import your code. This requires a MAJOR version change, not a PATCH. The is-promise incident occurred because version 2.2.0 introduced an exports field that broke existing import patterns. This should have been version 3.0.0.
2. Pre-Release Testing Requirements
Before publishing:
- Test against your own direct dependents (sample at least 50 if you have 500)
- Run integration tests in both CommonJS and ES module contexts
- Verify all documented import patterns still work
- Check that tree-shaking behavior hasn't changed
3. Deprecation Path for Breaking Changes
When breaking compatibility:
- Release a MINOR version that warns about deprecated patterns
- Wait at least 3 months before the MAJOR version
- Document the migration path in both CHANGELOG and README
For Package Consumers
1. Pin Major Versions Only
Your package.json should specify:
"is-promise": "^2.0.0"
NOT:
"is-promise": "*"
"is-promise": "latest"
The caret (^) allows MINOR and PATCH updates but blocks MAJOR changes.
2. Lock File Discipline
Commit your package-lock.json or yarn.lock. This ensures:
- CI/CD uses identical versions to local development
- You control when updates occur
- Rollback is possible when upstream breaks
3. Update Cadence
Run npm outdated weekly. Review and test updates in a branch before merging to main. This provides a controlled update window instead of emergency fixes when builds break.
Implementation Guidance
Setting Up Pre-Release Testing
Create a pre-publish.sh script:
#!/bin/bash
# Build the package
npm run build
# Pack it locally
npm pack
# Install in test project
cd ../test-consumer
npm install ../your-package/your-package-1.2.3.tgz
# Run consumer's test suite
npm test
Run this script before every npm publish.
Automated SemVer Validation
Use semantic-release or standard-version to enforce version bumps based on commit messages:
fix:commits → PATCH bumpfeat:commits → MINOR bumpBREAKING CHANGE:in commit body → MAJOR bump
This removes human judgment errors from versioning decisions.
Monitoring Dependency Health
Set up alerts for:
- New major versions of your direct dependencies
- Deprecation notices in
npm audit - Packages with no commits in 24+ months (abandonment risk)
GitHub's Dependabot can automate PATCH and MINOR update PRs. Review these weekly, but test them before merging.
Common Pitfalls
Pitfall 1: "It's just a config change"
Changing package.json fields like exports, main, or module is NOT a config change. These fields control how your code is loaded. Treat them as API surface.
Pitfall 2: Testing only in your own environment
The is-promise maintainer likely tested the change locally. But downstream consumers had different Node.js versions, different bundlers, different import patterns. Test across environments, not just your laptop.
Pitfall 3: Assuming PATCH means "safe"
Even PATCH versions can break if the maintainer misunderstands SemVer. Your CI pipeline should run full test suites on dependency updates, not just install them blindly.
Pitfall 4: Pinning to exact versions
Exact pinning ("is-promise": "2.1.0") prevents you from receiving security patches. Pin the MAJOR version, not the full version string.
Pitfall 5: No rollback plan
When a dependency breaks, you need to:
- Revert to the last known-good lock file
- Pin the working version explicitly
- File an issue upstream
- Monitor for a fix
Document this process before you need it.
Quick Reference Table
| Scenario | Version Bump | Example |
|---|---|---|
| Fix a bug, no API changes | PATCH | 2.1.0 → 2.1.1 |
| Add new function, old code still works | MINOR | 2.1.0 → 2.2.0 |
| Remove a function | MAJOR | 2.1.0 → 3.0.0 |
| Change module export mechanism | MAJOR | 2.1.0 → 3.0.0 |
| Modify function signature | MAJOR | 2.1.0 → 3.0.0 |
| Update dependencies (no API change) | PATCH | 2.1.0 → 2.1.1 |
| Change default behavior code relies on | MAJOR | 2.1.0 → 3.0.0 |
| Consumer Strategy | Risk Level | When to Use |
|---|---|---|
Pin exact version (2.1.0) |
High (no patches) | Never for production |
Pin major (^2.0.0) |
Low | Default for all deps |
Pin minor (~2.1.0) |
Medium | Unstable upstream |
Use latest |
Critical | Never |
| Testing Checkpoint | Frequency | Automation |
|---|---|---|
Run npm outdated |
Weekly | Dependabot |
| Review and test updates | Weekly | Manual |
| Full integration test on updates | Per PR | CI/CD |
| Test package before publish | Every release | pre-publish hook |
| Verify in consumer projects | Every release | Manual sampling |
The is-promise incident was resolved in three hours because the maintainer responded quickly. Your response time depends on having these processes in place before an incident occurs.



