Skip to content

A non-Error value thrown from a test environment's setup() is dropped from the "Failed to start worker" report (regression from 3.x) #10557

@brice-stacks

Description

@brice-stacks

Describe the bug

When a custom test environment's setup() rejects, the pool wraps the failure as
[vitest-pool]: Failed to start <pool> worker for test files … and attaches the
original value via { cause }. However, the reporter only prints the cause when it
is an Error instance:

  • throw new Error("…") → report includes Caused by: Error: …
  • throw "…" / throw { … } / a wasm-bindgen JsValue → the cause is silently dropped; the user sees only the generic, location-free pool message ❌

In Vitest 3.x the same non-Error throw was surfaced directly (Unknown Error: <value>),
so this is a regression. It appears to have come in with the worker-start error handling
added in #9337 / v4.0.17 (which correctly fixed the indefinite hang from #9271, but the
new wrapper only renders Error-typed causes).

This bites real-world environments that initialize native/wasm resources in setup()
and reject with a non-Error value. For example @stacks/clarinet-sdk's
vitest-environment-clarinet calls a wasm-bindgen initSimnet() in setup(); when a
contract fails to compile, the wasm rejects with a JsValue, and on v4 the actual
diagnostic is completely lost — you only see Failed to start forks worker.

Reproduction

https://stackblitz.com/edit/vitest-dev-vitest-ldzxobzz?file=throwing-env.js

package.json

{
  "name": "vitest-env-setup-error-swallowed",
  "version": "1.0.0",
  "type": "module",
  "private": true,
  "scripts": {
    "test": "vitest run"
  },
  "devDependencies": {
    "vitest": "^4.1.8"
  }
}

throwing-env.js

// Minimal custom test environment whose async setup() rejects with a NON-Error value.
// Real-world analogue: wasm-bindgen / native addons reject with a plain JsValue
// (e.g. @stacks/clarinet-sdk's simnet init), not an `Error` instance.
export default {
  name: "throwing-env",
  transformMode: "ssr",
  async setup() {
    // A proper `throw new Error("...")` IS surfaced by vitest (as `Caused by:`).
    // A non-Error value is silently dropped — only the generic pool error remains.
    throw "the real reason setup failed (a non-Error value)";
  },
};

vitest.config.js

import { defineConfig } from "vitest/config";

export default defineConfig({
  test: {
    environment: "./throwing-env.js",
    // pool: "forks" is the default; the message mentions "forks worker".
  },
});

test/example.test.js

import { test, expect } from "vitest"; test("noop", () => { expect(1).toBe(1); });

Run npm install and npm test.

(Swap the throw for throw new Error("…") to see the cause correctly printed — that
contrast shows the wrapping/cause machinery is present; only non-Error rendering is
missing.)

System Info

System:
    OS: macOS 26.5.1
    CPU: (14) arm64 Apple M4 Pro
    Memory: 120.22 MB / 48.00 GB
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 25.1.0 - /Users/brice/.nvm/versions/node/v25.1.0/bin/node
    npm: 11.6.2 - /Users/brice/.nvm/versions/node/v25.1.0/bin/npm
  Browsers:
    Chrome: 149.0.7827.55
    Safari: 26.5
  npmPackages:
    vitest: ^4.1.8 => 4.1.8

Used Package Manager

npm

Validations

Metadata

Metadata

Assignees

No one assigned

    Type

    No fields configured for Bug.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions