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. 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 --<span class="hljs-built_in">test
<span class="hljs-comment"># With Jest
c8 jest
<span class="hljs-comment"># With Mocha
c8 mocha tests/**/*.spec.js
<span class="hljs-comment"># With any command
c8 npm <span class="hljs-built_in">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 --<span class="hljs-built_in">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
<span class="hljs-comment"># 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 <span class="hljs-string">'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
<span class="hljs-comment"># 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