vitest-dev/vitest · error · Error

Runner must export a default function, but got

Error message

Runner must export a default function, but got ${typeof mod.default} imported from ${config.runner}

What it means

Thrown by getTestRunnerConstructor when the module referenced by the config `runner` option is imported successfully but has no usable default-exported constructor function. Vitest instantiates the custom runner via `new mod.default(config)`, so the default export must be a class or function. The message reports the actual typeof the default export for diagnosis.

Solutions

  1. Add `export default` to the runner class: `export default class MyRunner extends VitestTestRunner { ... }`.
  2. If the runner is a named export, re-export it as default: `export { MyRunner as default } from './MyRunner'`.
  3. Verify `config.runner` points to the file that contains the class, not a config or barrel file.
  4. Ensure the default export is a constructor (class or function), not an instance or plain object.

Example fix

// before — my-runner.ts
export class MyRunner extends VitestTestRunner { ... }

// after
export default class MyRunner extends VitestTestRunner { ... }
Defensive patterns

Strategy: type-guard

Validate before calling

import { importMeta } from './loader' // your dynamic import helper
async function loadRunner(path: string) {
  const mod = await import(/* @vite-ignore */ path)
  if (typeof mod.default !== 'function') {
    throw new TypeError(`Custom runner at ${path} must default-export a constructor`)
  }
  return mod.default
}

Type guard

import type { VitestRunnerConstructor } from 'vitest/runners'
function isRunnerCtor(
  mod: any,
): mod is VitestRunnerConstructor {
  return typeof mod?.default === 'function'
}

Prevention

When it happens

Trigger: Setting `runner: './my-runner.ts'` in vitest config where that file has no default export, exports a default that is an object/value (not callable), or uses a named export instead of default.

Common situations: Writing a custom runner and forgetting `export default class MyRunner`; exporting the runner as a named export (`export class MyRunner`); default-exporting a configuration object instead of the class; pointing `runner` at the wrong file path.

Related errors


AI-assisted analysis of vitest-dev/vitest@1fa9837ec2 (2026-08-11). Data as JSON: /api/errors/7960cf0a824dcf1c. Report an issue: GitHub.

Appendix: source

Thrown at packages/vitest/src/runtime/runners/index.ts:20

import type { SerializedConfig } from '../config'
import type { TestModuleRunner } from '../moduleRunner/testModuleRunner'
import type { VitestRunner, VitestRunnerConstructor } from '../runner/types'
import { takeCoverageInsideWorker } from '../../integrations/coverage'
import { rpc } from '../rpc'
import { loadDiffConfig, loadSnapshotSerializers } from '../setup-common'
import { getWorkerState } from '../utils'
import { TestRunner } from './test'

async function getTestRunnerConstructor(
  config: SerializedConfig,
  moduleRunner: TestModuleRunner,
): Promise<VitestRunnerConstructor> {
  if (!config.runner) {
    return TestRunner as any as VitestRunnerConstructor
  }
  const mod = await moduleRunner.import(config.runner)
  if (!mod.default && typeof mod.default !== 'function') {
    throw new Error(
      `Runner must export a default function, but got ${typeof mod.default} imported from ${
        config.runner
      }`,
    )
  }
  return mod.default as VitestRunnerConstructor
}

export async function resolveTestRunner(
  config: SerializedConfig,
  moduleRunner: TestModuleRunner,
  traces: Traces,
): Promise<VitestRunner> {
  const TestRunner = await getTestRunnerConstructor(config, moduleRunner)
  const testRunner = new TestRunner(config)

  // inject private executor to every runner
  Object.defineProperty(testRunner, 'moduleRunner', {

View on GitHub (pinned to 1fa9837ec2)