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/**",
]
Coverage output showed everything green:
All files | 63.40 | 80.03 |
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
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);
}
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.
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
-
oven-sh/bun#17028 —
coverageThresholdis per-file, not overall (labeledbug) - oven-sh/bun#5099 — Feature request for per-folder threshold control
- oven-sh/bun#21395 — Feature request for test file exclusion
Takeaways
- If
bun test --coverageexits 1 with all tests passing, the culprit is almost certainly a per-file coverage check on a low-coverage file. - Always specify
functions = 0if you only care about line coverage — otherwise bun applies a phantom function threshold. - Parse the coverage output yourself if you need overall (not per-file) threshold enforcement.
- 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)