c8: Native V8 Code Coverage for Node.js Without Instrumentation

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 c8

Basic 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">test

c8 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">test

Add 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.html

The 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.info

Excluding 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=v8

Comparing 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/**/*.js

c8 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=text

Full 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: true

Key Takeaways

  • c8 uses V8's built-in coverage — no code transformation, works natively with ESM
  • Prefix any test command with c8 to get coverage with no other changes
  • Set thresholds (--branches, --functions, --lines) to gate CI on coverage drops
  • Use --reporter=lcov for Codecov/Coveralls integration
  • Use /* c8 ignore next */ to exclude specific lines from coverage requirements

Read more