Istanbul / nyc: JavaScript Code Coverage Setup & Best Practices
Istanbul is the most widely used JavaScript code coverage tool. Its CLI wrapper nyc makes it trivial to add coverage instrumentation to any Node.js project. This guide covers setup, configuration, coverage thresholds, CI integration, and how to actually use coverage data to improve your tests.
What Istanbul/nyc Measures
Istanbul tracks four coverage metrics:
- Statements — each executable statement in your code
- Branches — each conditional path (if/else, ternary, logical operators)
- Functions — each function or method definition
- Lines — each line containing executable code
A common misconception: 100% line coverage doesn't mean 100% branch coverage. A function with an if/else can have both lines executed but one branch never taken.
Installation
# Using nyc (the Istanbul CLI)
npm install --save-dev nyc
<span class="hljs-comment"># Or with mocha
npm install --save-dev mocha nyc
<span class="hljs-comment"># For Jest projects — Istanbul is built in, no extra package neededIstanbul works natively with CommonJS. For ES modules and TypeScript, additional configuration is required (covered below).
Basic Setup
package.json Configuration
{
"scripts": {
"test": "mocha tests/**/*.test.js",
"coverage": "nyc mocha tests/**/*.test.js",
"coverage:report": "nyc report --reporter=html"
},
"nyc": {
"include": ["src/**/*.js"],
"exclude": ["tests/**", "node_modules/**"],
"reporter": ["text", "lcov", "html"],
"all": true
}
}Run coverage:
npm run coverageOutput:
----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 87.50 | 75.00 | 83.33 | 87.50 |
src/ | | | | |
auth.js | 85.00 | 70.00 | 80.00 | 85.00 | 24-28, 67
utils.js| 90.00 | 80.00 | 87.50 | 90.00 | 45
----------|---------|----------|---------|---------|-------------------Configuration Options
.nycrc or .nycrc.json
Keep Istanbul config in a separate file for cleaner package.json:
{
"include": ["src/**/*.js"],
"exclude": [
"**/*.test.js",
"**/*.spec.js",
"src/generated/**",
"src/migrations/**"
],
"extension": [".js", ".jsx"],
"reporter": ["text", "lcov", "html", "json"],
"all": true,
"check-coverage": true,
"branches": 80,
"functions": 85,
"lines": 85,
"statements": 85,
"temp-dir": "./.nyc_output"
}The all: true flag is critical — without it, Istanbul only reports coverage for files that are actually imported during tests. Files with 0% coverage won't appear at all, creating a false sense of coverage completeness.
Coverage Thresholds
Setting check-coverage: true with threshold values causes nyc to exit with a non-zero code when coverage falls below the specified percentage. This integrates cleanly with CI:
{
"check-coverage": true,
"branches": 75,
"functions": 80,
"lines": 80,
"statements": 80
}If coverage drops below threshold:
ERROR: Coverage for branches (68.75%) does not meet global threshold (75%)
npm ERR! Test failed. See above for more details.Recommended Thresholds
Starting from scratch:
- New projects: 70%+ lines, 60%+ branches as a baseline
- Mature projects: 80–85%+ lines, 75%+ branches
- Critical path code: Consider 90%+ for auth, payment, or data validation
Don't chase 100%. Some code (error handling for truly exceptional cases, third-party adapter boilerplate) adds test complexity for minimal risk reduction.
TypeScript Support
Istanbul doesn't instrument TypeScript directly — you need source maps to map coverage back to .ts files:
npm install --save-dev ts-node @istanbuljs/schema source-map-support.nycrc.json:
{
"extension": [".ts", ".tsx"],
"include": ["src/**/*.ts"],
"exclude": ["**/*.test.ts", "**/*.spec.ts", "src/types/**"],
"reporter": ["text", "lcov", "html"],
"all": true,
"require": ["ts-node/register", "source-map-support/register"]
}Run with:
npx nyc mocha --require ts-node/register tests/**/*.test.tsES Module Support
ES modules require additional configuration since Istanbul historically targeted CommonJS:
npm install --save-dev c8c8 is a newer coverage tool that uses Node.js's built-in V8 coverage — no instrumentation needed, works natively with ES modules:
{
"scripts": {
"coverage": "c8 --reporter=text --reporter=lcov node --experimental-vm-modules node_modules/.bin/mocha tests/**/*.test.mjs"
}
}For mixed CJS/ESM projects, c8 is generally easier than configuring Istanbul's experimental ESM support.
Reporters
Istanbul supports multiple output formats:
| Reporter | Output | Use Case |
|---|---|---|
text |
Terminal table | Local development |
text-summary |
One-line summary | Quick CI check |
lcov |
lcov.info file |
Codecov, Coveralls upload |
html |
coverage/ folder |
Detailed local inspection |
json |
coverage-final.json |
Programmatic consumption |
clover |
Clover XML | Jenkins coverage reports |
cobertura |
Cobertura XML | Azure DevOps, GitLab |
For CI, combine text-summary (to see numbers in logs) and lcov (to upload to a coverage service):
{
"reporter": ["text-summary", "lcov"]
}CI/CD Integration
GitHub Actions
name: Tests with Coverage
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- name: Run tests with coverage
run: npm run coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage/lcov.info
fail_ci_if_error: trueGitLab CI with Coverage Badge
test:
script:
- npm ci
- npx nyc --reporter=text --reporter=lcov mocha tests/**/*.test.js
- npx nyc report --reporter=text-summary
coverage: '/Lines\s*:\s*(\d+\.?\d*)/'
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xmlExcluding Code from Coverage
Some code shouldn't be covered — configuration files, generated code, type definitions:
File-level exclusion
In .nycrc.json:
{
"exclude": [
"src/generated/**",
"src/config/**",
"**/__mocks__/**"
]
}Inline exclusion with comments
/* istanbul ignore next */
function legacyFallback() {
// Never runs in modern environments
}
// Single branch:
const value = condition
? doThis()
: /* istanbul ignore next */ doThatNever();
// Entire block:
/* istanbul ignore if */
if (process.env.NODE_ENV === 'development') {
enableDevTools();
}Use inline ignores sparingly. Overuse is a code smell — it usually means the code is untestable rather than genuinely unreachable.
Reading Coverage Reports
The HTML report (run npx nyc report --reporter=html, open coverage/index.html) highlights uncovered lines in red. Common patterns to investigate:
Uncovered error branches:
async function fetchUser(id) {
try {
return await db.find(id);
} catch (err) { // ← this branch often uncovered
throw new ApiError(500, 'DB error');
}
}Add a test that simulates a database failure.
Uncovered null guards:
function formatName(user) {
if (!user) return 'Anonymous'; // ← uncovered if tests always pass a user
return `${user.first} ${user.last}`;
}Add a test with formatName(null).
Dead code:
function process(data) {
// ... main logic ...
return result;
// This line never runs — actual dead code
cleanup(data);
}Delete it.
Common Pitfalls
all: false giving misleading numbers. Without all: true, files not imported during tests don't show up. Your 90% coverage might be 40% if half the codebase was never even loaded.
Counting mocks as coverage. Test doubles and mock files in src/ can inflate coverage artificially. Exclude __mocks__/ directories.
Treating coverage as quality. A test that calls a function but doesn't assert anything will contribute to coverage but not to quality. Coverage measures what code runs, not whether it's tested correctly.
Confusing statement and branch coverage. if (a && b) has one statement but three branches: a=false, a=true b=false, a=true b=true. Statement coverage misses this; branch coverage catches it.
Beyond Unit Test Coverage
Istanbul/nyc measures coverage from your unit tests. For end-to-end coverage (what code runs during browser-based tests), you need a different approach.
HelpMeTest runs end-to-end tests against your live application and tracks which user journeys are covered. Where Istanbul tells you which lines execute in unit tests, HelpMeTest tells you whether your actual user flows work in production. Together they give a complete picture — code-level coverage and real-world behavior validation.
Summary
Istanbul/nyc setup in three steps:
npm install --save-dev nyc- Add
.nycrc.jsonwithinclude,exclude,reporter,all: true, and thresholds - Replace
npm testwithnyc npm testin CI
Key practices:
- Always set
all: trueto surface files with 0% coverage - Use branch coverage thresholds, not just line coverage
- Set coverage as a CI gate to prevent regression
- Use
lcovreporter to upload to Codecov or Coveralls - Read the HTML report to understand what's actually uncovered, not just the numbers