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

  1. Move the expect.soft(...) call inside an it() or test() block so a Test context is active.
  2. If the assertion is in a setup/teardown hook (beforeAll/afterAll/etc.), switch to plain expect() since soft failures cannot be recorded there.
  3. 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.
  4. 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

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


AI-assisted analysis of vitest-dev/vitest@d568f8ce37 (2026-08-03). Data as JSON: /data/errors/91a1e0a9c9f1551b.json. Report an issue: GitHub.