c8: Native V8 Code Coverage for Node.js Without Instrumentation
c8 (coverage using V8) measures code coverage by reading the coverage data that Node.js's V8 engine generates natively.
c8 (coverage using V8) measures code coverage by reading the coverage data that Node.js's V8 engine generates natively. Unlike Istanbul/nyc, it requires no code transformation or instrumentation — your source runs as-is, making it compatible with ES modules and faster to execute.
How c8 Differs from Istanbul/nyc
| c8 | Istanbul/nyc | |
|---|---|---|
| Mechanism | V8 built-in coverage | Code instrumentation |
| ESM support | Native | Requires transpilation |
| Performance | No overhead | ~5–20% slower |
| Source maps | Supported | Supported |
| Configuration | Minimal | Extensive |
Installing c8
npm install --save-dev c8Basic Usage
Prefix any test command with c8:
# Run tests with coverage
c8 node --test
# With Jest
c8 jest
# With Mocha
c8 mocha tests/**/*.spec.js
# With any command
c8 npm testc8 collects coverage while your tests run and prints a summary when they finish.
Coverage Report
After running, c8 prints a table:
----------|---------|----------|---------|---------|
File | % Stmts | % Branch | % Funcs | % Lines |
----------|---------|----------|---------|---------|
src/ | | | | |
math.js | 100 | 100 | 100 | 100 |
user.js | 85.7 | 75.0 | 100 | 85.7 |
----------|---------|----------|---------|---------|Enforcing Thresholds
Fail the process if coverage drops below a threshold:
c8 --branches 80 --functions 90 --lines 85 --statements 85 node --testAdd to package.json:
{
"scripts": {
"test": "c8 --branches 80 --functions 90 --lines 85 node --test",
"test:coverage": "c8 --reporter=html node --test"
}
}If coverage is below the threshold, c8 exits with code 1 — CI fails automatically.
c8 Configuration File
Create .c8rc.json or add to package.json:
{
"c8": {
"include": ["src/**/*.js"],
"exclude": ["src/**/*.test.js", "src/generated/**"],
"reporter": ["text", "lcov", "html"],
"branches": 80,
"functions": 90,
"lines": 85,
"statements": 85,
"all": true
}
}Options:
| Option | Description |
|---|---|
include |
Glob patterns for files to measure |
exclude |
Patterns to exclude (test files, generated code) |
reporter |
Output formats: text, html, lcov, json, clover, cobertura |
all |
Include files not touched by tests (shows 0% coverage) |
src |
Source root for resolving paths |
Source Maps
c8 automatically follows source maps to report coverage on original TypeScript or bundled source:
{
"c8": {
"include": ["src/**/*.ts"],
"reporter": ["text", "lcov"]
}
}No additional configuration needed if your TypeScript/build tooling emits source maps alongside output files.
HTML Report
Generate an interactive HTML report for detailed line-by-line inspection:
c8 --reporter=html node --test
open coverage/index.htmlThe HTML report highlights covered (green), uncovered (red), and partial (yellow) lines.
LCOV for CI Upload
Generate LCOV format for uploading to Codecov, Coveralls, or similar:
c8 --reporter=lcov node --test
# Creates coverage/lcov.info# .github/workflows/test.yml
- name: Run tests with coverage
run: c8 --reporter=text --reporter=lcov node --test
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
files: ./coverage/lcov.infoExcluding Files from Coverage
Exclude test utilities, generated code, and vendor files:
{
"c8": {
"exclude": [
"tests/**",
"coverage/**",
"src/generated/**",
"node_modules/**",
"*.config.js"
]
}
}You can also exclude specific lines with comments:
/* c8 ignore next */
if (process.env.DEBUG) {
console.log(debug);
}
/* c8 ignore start */
function legacyCompat() {
// old code path, intentionally untested
}
/* c8 ignore stop */Combining c8 with Different Test Runners
With node:test (built-in)
c8 node --test 'src/**/*.test.js'With Mocha
c8 mocha --recursive tests/With AVA
AVA manages its own worker processes — use its built-in coverage passthrough:
{
"ava": {
"nodeArguments": ["--experimental-vm-modules"]
},
"scripts": {
"test": "c8 ava"
}
}With Vitest
Vitest has its own V8 coverage provider that's separate from c8. Use Vitest's built-in:
vitest run --coverage.provider=v8Comparing c8 vs nyc
If you're migrating from nyc:
# Before (nyc)
nyc --branches 80 --reporter=lcov mocha tests/**/*.js
# After (c8)
c8 --branches 80 --reporter=lcov mocha tests/**/*.jsc8 flags mirror nyc's interface, making migration straightforward. The main difference: c8 doesn't need nyc instrument for pre-instrumentation steps — remove those from your build pipeline.
Checking Coverage Without Running Tests
Check the coverage of a previous run stored in .v8-coverage/:
c8 report --reporter=textFull CI Setup
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
- run: npm ci
- name: Run tests with coverage
run: |
c8 \
--branches 80 \
--functions 90 \
--lines 85 \
--statements 85 \
--reporter=text \
--reporter=lcov \
node --test 'src/**/*.test.js'
- name: Upload to Codecov
uses: codecov/codecov-action@v4
with:
files: ./coverage/lcov.info
fail_ci_if_error: trueKey Takeaways
- c8 uses V8's built-in coverage — no code transformation, works natively with ESM
- Prefix any test command with
c8to get coverage with no other changes - Set thresholds (
--branches,--functions,--lines) to gate CI on coverage drops - Use
--reporter=lcovfor Codecov/Coveralls integration - Use
/* c8 ignore next */to exclude specific lines from coverage requirements