vitest-dev/vitest · error · Error
expect.soft() can only be used inside a test
Error message
expect.soft() can only be used inside a test
What it means
Thrown by wrapAssertion in packages/expect/src/utils.ts:121 when the 'soft' flag is set on an assertion but no 'vitest-test' flag is present. expect.soft() records failures onto a Test result object instead of throwing immediately, so it needs an active test context to attach those deferred failures to. Without a test in scope there is nowhere to record the soft failure, so the call is rejected.
Source
Thrown at packages/expect/src/utils.ts:121
if (name !== 'withTest') {
utils.flag(this, '_name', name)
}
if (!utils.flag(this, 'soft')) {
// avoid WebKit's proper tail call to preserve stacktrace offset for inline snapshot
// https://webkit.org/blog/6240/ecmascript-6-proper-tail-calls-in-webkit
try {
return fn.apply(this, args)
}
finally {
// no lint
}
}
const test: Test = utils.flag(this, 'vitest-test')
if (!test) {
throw new Error('expect.soft() can only be used inside a test')
}
try {
const result = fn.apply(this, args)
if (result && typeof result === 'object' && typeof result.then === 'function') {
return result.then(noop, (err) => {
handleTestError(test, err)
})
}
return result
}
catch (err) {
handleTestError(test, err)
}
}
}View on GitHub (pinned to d568f8ce37)
Solutions
- Move the expect.soft(...) call inside an it() or test() block so a Test context is active.
- If the assertion is in a setup/teardown hook (beforeAll/afterAll/etc.), switch to plain expect() since soft failures cannot be recorded there.
- If the assertion lives in a shared helper, pass the test context explicitly or restructure so the helper is only invoked from within it blocks.
- Verify you are running the file through vitest (not node) so the test binding is injected onto the assertion.
Example fix
// before
beforeAll(() => {
expect.soft(db.tables).toHaveLength(3)
})
// after
beforeAll(() => {
expect(db.tables).toHaveLength(3)
})
// or move it inside the test:
it('has tables', () => {
expect.soft(db.tables).toHaveLength(3)
}) Defensive patterns
Strategy: validation
Validate before calling
// Ensure expect.soft is only called within an active test context.
import { expect } from 'vitest'
function softAssertIfInTest<T>(check: () => T) {
// Vitest does not expose the active test publicly; gate by convention:
// only call this helper from inside it/test blocks.
if (typeof (globalThis as any).__vitest_test__ === 'undefined') {
return // skip soft assertion outside a test
}
try { expect.soft(check()) } catch { /* soft handles it */ }
} Type guard
// No public type guard exists for the internal 'vitest-test' chai flag.
// Convention guard: only invoke expect.soft inside it()/test().
function isInsideTest(): boolean {
// Approximate: rely on Vitest setting context. Best practice is structural
// (call site is lexically inside it/test), not runtime-checked.
return true
} Try / catch
// If you must run an assertion in a context that may or may not be a test,
// fall back to a hard expect when soft is unavailable.
function assert<T>(value: T, matcher: (e: ReturnType<typeof expect>) => void) {
try {
matcher(expect.soft(value) as any)
} catch (e) {
if (e instanceof Error && /can only be used inside a test/.test(e.message)) {
matcher(expect(value) as any)
} else { throw e }
}
} Prevention
- Only call expect.soft inside it()/test() blocks; never in beforeAll/afterAll/beforeEach/afterEach.
- When extracting assertion helpers, document that they must be invoked from within a test.
- Run a quick grep for 'expect.soft' in hook files during code review.
When it happens
Trigger: Calling expect.soft(...) at module top level, inside beforeAll/beforeEach/afterAll/afterEach hooks, inside describe-only callback without an it, or in a plain Node script invoked outside the Vitest runner. The check at utils.ts:118-122 fires whenever utils.flag(this, 'vitest-test') returns falsy while utils.flag(this, 'soft') is truthy.
Common situations: Refactoring a test and accidentally moving a soft assertion into a setup hook; calling expect.soft from a shared helper invoked outside any it block; migrating from plain expect to expect.soft without verifying call sites; running test files via node directly instead of vitest.
Related errors
- Invalid element or locator: ${elementOrLocator}. Expected an
- .toContainHTML() expects a string value, got ${htmlText}
- Exact option does not support RegExp expected class names
- .toHaveDisplayValue() currently supports only input, textare
- .toHaveDisplayValue() currently does not support input[type=
AI-assisted analysis of vitest-dev/vitest@d568f8ce37 (2026-08-03).
Data as JSON: /data/errors/91a1e0a9c9f1551b.json.
Report an issue: GitHub.