DEV Community

Waclaw Kusnierczyk
Waclaw Kusnierczyk

Posted on

Bun's Coverage Threshold

Bun's Coverage Threshold: Three Undocumented Behaviors That Will Waste Your Afternoon

Date: 2026-02-08

On a private project, we spent a full debugging session chasing down why bun test --coverage exits with code 1 despite all 757 tests passing and overall line coverage sitting at 80% — comfortably above our 80% threshold. There was no error message and no indication of what failed. Just a silent exit code 1 annoyingly breaking the CI.

Here's what we found.

The Setup

Our bunfig.toml looked perfectly reasonable:

[test]
coverageThreshold = { lines = 0.8 }
coverageSkipTestFiles = true
coveragePathIgnorePatterns = [
  "src/app/**",
  "src/components/**",
]
Enter fullscreen mode Exit fullscreen mode

Coverage output showed everything green:

All files  |   63.40 |   80.03 |
Enter fullscreen mode Exit fullscreen mode

80.03% lines, threshold is 80%. Should pass. Doesn't.

Gotcha 1: The threshold is per-file, not overall

The docs say:

To require 90% line-level and function-level coverage: coverageThreshold = 0.9

This implies it checks the "All files" aggregate. It doesn't. Bun checks every single file individually against the threshold. If you have a utility module at 19% line coverage and your threshold is 80%, the build fails — even if your overall coverage is well above 80%.

In our project, files like rag-lookup.ts (2.7% lines) and ollama-client.ts (19.7% lines) were dragging things down per-file while the overall sat at 80%.

This is oven-sh/bun#17028, filed Feb 2025, labeled bug, still open.

Gotcha 2: Specifying lines implicitly enforces a function threshold

Even after we dropped the threshold to { lines = 0.5 } (way below our actual coverage), it still failed. We swept through threshold values:

functions=0.0   EXIT=0   <-- only this passes
functions=0.05  EXIT=1
functions=0.1   EXIT=1
functions=0.3   EXIT=1
functions=0.5   EXIT=1
Enter fullscreen mode Exit fullscreen mode

Specifying any lines value in the threshold object causes bun to also enforce a function coverage check with some implicit default. Many of our files have 0% function coverage (bun counts top-level code as "line" coverage but not "function" coverage), so any non-zero function threshold fails them.

The fix: coverageThreshold = { lines = 0.8, functions = 0 }. But since gotcha #1 makes this per-file anyway, it still doesn't do what we want.

This behavior is completely undocumented. The docs show { lines = 0.9, functions = 0.9 } as an example but never mention what happens to unspecified metrics.

Gotcha 3: No error output whatsoever

When the threshold check fails, bun prints the normal coverage table and then exits with code 1. No message saying "file X has Y% coverage, below threshold Z%". No indication that the threshold is what failed vs. a test failure. Just a silent non-zero exit.

Combined with gotchas 1 and 2, this means you see:

  • All tests pass
  • Overall coverage above threshold
  • Exit code 1
  • No explanation

Our Workaround

We removed coverageThreshold from bunfig.toml entirely and enforce the threshold in a custom script that parses the "All files" line:

// scripts/update-coverage-report.ts
const REQUIRED_LINE_PCT = 80;
if (allLines < REQUIRED_LINE_PCT) {
  console.error(
    `\n✗ Overall line coverage ${allLines.toFixed(1)}% is below the ${REQUIRED_LINE_PCT}% threshold`,
  );
  process.exit(1);
}
Enter fullscreen mode Exit fullscreen mode

The script runs bun test --coverage, parses the text output, checks the overall aggregate, and prints a clear error message if it fails. CI calls this script instead of relying on bun's built-in threshold.

# bunfig.toml
[test]
# NOTE: We do NOT use coverageThreshold here because bun enforces it
# per-file (not overall), and many utility files have low coverage.
# Overall threshold is enforced in scripts/update-coverage-report.ts instead.
Enter fullscreen mode Exit fullscreen mode

Bonus: [test].exclude doesn't exist either

While debugging this, we also discovered that [test].exclude in bunfig.toml is silently ignored — it's not a real option. Bun's test runner has no built-in way to exclude test files from discovery. The workaround is describe.skipIf() at runtime. Tracked in oven-sh/bun#21395.

Upstream Issues

Takeaways

  1. If bun test --coverage exits 1 with all tests passing, the culprit is almost certainly a per-file coverage check on a low-coverage file.
  2. Always specify functions = 0 if you only care about line coverage — otherwise bun applies a phantom function threshold.
  3. Parse the coverage output yourself if you need overall (not per-file) threshold enforcement.
  4. Bun is still young. Its test runner has rough edges. When something doesn't work, check whether the config option actually exists before assuming your config is wrong.

Top comments (0)