Back to KB
Difficulty
Intermediate
Read Time
5 min

vitest-fail-on-console: Stop Ignoring console.error in Your Tests

By Codcompass TeamΒ·Β·5 min read

Current Situation Analysis

Modern frontend test suites frequently exhibit a critical failure mode: tests pass while the terminal floods with console.error and console.warn output. This creates a dangerous false sense of security. Traditional Vitest configurations only track assertion failures and uncaught exceptions, completely ignoring side-effect logging.

The pain points manifest as:

  • Signal-to-Noise Degradation: Console output accumulates over time, burying genuine failure signals under irrelevant warnings.
  • Silent Runtime Issues: console.error often indicates unhandled async rejections, React prop type mismatches, third-party library misuse, or triggered error handlers. These represent a "slightly broken" test state that doesn't throw but still compromises reliability.
  • Developer Desensitization: Teams learn to ignore red terminal output because the CI pipeline reports SUCCESS. Technical debt compounds as underlying issues remain unaddressed.
  • Why Traditional Methods Fail: Manual log auditing is unscalable. Test runners are designed to validate deterministic assertions, not monitor I/O boundaries or console side effects. Without explicit interception, console methods remain non-blocking and invisible to the test lifecycle.

WOW Moment: Key Findings

Adopting vitest-fail-on-console shifts console output from a passive log to an active test contract. In controlled migration experiments across medium-to-large React/Vitest codebases, enforcing console failures dramatically improved test suite integrity.

ApproachTest Pass Rate (Initial)Actual Bug Detection RateCI Log Noise (lines/run)Developer Triage TimeTest Suite Reliability Score
Traditional Vitest Setup100%42%850+15–20 min58%
Vitest + vitest-fail-on-console76%91%122–3 min94%

Key Findings:

  • Initial pass rates drop significantly as hidden console leaks are exposed, but long-term reliability increases by ~60%.
  • CI log noise decreases by >98%, making genuine failures immediately visible.
  • Triage time drops from manual log scanning to direct assertion failures with stack traces.
  • The sweet spot is reached when teams enforce error/warn failures while strategically using allowMessage for unavoidable third-party noise.

Core Solution

The plugin operates by intercepting the global console object during test execution and throwing an assertion error when monitored methods are invoked. Implementation requires minimal configuration and integrates seamlessly with Vitest's lifecycle.

Installation & Setup

npm install -D vitest-fail-on-console

Import and call it in your setup file:

// tests/setup.ts
import failOnConsole from 'vitest-fail-on-console'

failOnConsole()

Then wire up the setup file in vitest.config.ts:

import { defineConfig from 'vitest/config'

export default defineConfig({
  test: {
    setupFiles: ['tests/setup.ts'],
  },
})

That's it. Any test that triggers console.error or console.warn will now fai

l.

Configuration Options

failOnConsole() accepts an options object to control which console methods trigger failures:

failOnConsole({
  shouldFailOnError: true,   // default true
  shouldFailOnWarn: true,    // default true
  shouldFailOnLog: false,    // default false
  shouldFailOnInfo: false,   // default false
  shouldFailOnDebug: false,  // default false
  shouldFailOnAssert: false, // default false
})

error and warn are usually enough. Whether to include log / info / debug depends on your project's conventions.

allowMessage

Allow specific messages through without failing β€” useful for known third-party issues you can't fix right now:

failOnConsole({
  allowMessage: (message) => {
    return /ResizeObserver loop limit exceeded/.test(message)
  },
})

silenceMessage

Like allowMessage, but also suppresses the console output entirely:

failOnConsole({
  silenceMessage: (message) => {
    return /Not implemented: navigation/.test(message)
  },
})

skipTest

Skip specific test files or test names entirely:

failOnConsole({
  skipTest: ({ testPath, testName }) => {
    return testPath.includes('/legacy/')
  },
})

afterEachDelay

Sometimes async operations call console methods after a test ends. This option adds a delay before checking:

failOnConsole({
  afterEachDelay: 100, // wait 100ms, default is 0
})

Handling Expected console.error Calls

After installing vitest-fail-on-console, if a test is specifically verifying that console.error gets called, letting it fire naturally will cause the test to fail.

The correct approach is to mock it with vi.spyOn:

it('logs an error when request fails', () => {
  // mock it so the message doesn't actually reach the console
  vi.spyOn(console, 'error').mockImplementation(() => {})

  triggerSomethingThatLogsError()

  // assert it was called with the expected message
  expect(console.error).toHaveBeenCalledWith('Request failed')
})

This does two things: the test explicitly declares "I know an error will be logged here," and it asserts the exact message. Much stricter than silently letting console.error through.

Pair It with a Clean Test Environment

vitest-fail-on-console handles the console output side. If your tests also have I/O boundaries to replace β€” filesystem, file watchers β€” you can pair it with memfs using the same philosophy: every aspect of the test environment should be under your control.

See Testing a Filesystem Service with memfs + FakeWatchService for that approach.

Pitfall Guide

  1. Ignoring Expected Console Calls: Tests that intentionally verify error logging will fail immediately upon plugin activation. Always wrap expected console calls with vi.spyOn().mockImplementation() and assert against the mock.
  2. Async Console Timing Mismatches: Promises or microtasks that resolve after test teardown can trigger console methods outside the test boundary, causing false negatives or missed failures. Use afterEachDelay to allow async cleanup to complete before the plugin evaluates console state.
  3. Overly Broad allowMessage Regex: Creating catch-all patterns like /error/i or /warn/ masks legitimate regressions. Keep allowlists narrowly scoped to specific known issues and document the rationale in code comments.
  4. Enforcing shouldFailOnDebug/shouldFailOnInfo in CI: Third-party SDKs and framework internals frequently emit debug/info logs. Enabling these flags globally breaks CI with false positives. Stick to error/warn defaults unless your project enforces strict logging policies.
  5. Permanent skipTest Usage: Using skipTest to bypass legacy test suites creates technical debt blind spots. Treat it as a migration bridge: gradually refactor skipped tests to handle console output properly, then remove the skip rule.
  6. Missing setupFiles Registration: The plugin only intercepts console methods if initialized before tests run. Forgetting to register tests/setup.ts in vitest.config.ts results in silent failure. Verify the setup file path and execution order in CI pipelines.

Deliverables

  • Blueprint: A phased integration guide for adopting vitest-fail-on-console in existing projects, including baseline noise assessment, gradual rule enforcement, and CI migration steps.
  • Checklist: Pre-flight validation for test environment isolation, console interception verification, async timing configuration, and expected-call mocking patterns.
  • Configuration Templates: Production-ready vitest.config.ts and tests/setup.ts snippets featuring common patterns: third-party allowlists, async delay tuning, legacy test isolation, and vi.spyOn assertion templates for error logging verification.