vitest-dev/vitest · error · AssertionError

expected promise to throw an error, but it didn't

Error message

expected promise to throw an error, but it didn't

What it means

Thrown by the 'throw'/'throws'/'Throw' assertion overwrite when a promise flagged as 'resolves' resolves to a value that is not a function. The library expects the resolved value to be a function that itself throws (the .resolves[.not].toThrow() pattern), and since a non-function cannot throw, the assertion fails immediately. This is distinct from the duplicate-looking line 774, which fires for the .toThrow(Error|errorObject) overload.

Source

Thrown at packages/expect/src/jest-expect.ts:90

        const object = utils.flag(this, 'object')
        const isNot = utils.flag(this, 'negate') as boolean
        if (promise === 'rejects') {
          utils.flag(this, 'object', () => {
            throw object
          })
        }
        // if it got here, it's already resolved
        // unless it tries to resolve to a function that should throw
        // called as '.resolves[.not].toThrow()`
        else if (promise === 'resolves' && typeof object !== 'function') {
          if (!isNot) {
            const message
              = utils.flag(this, 'message')
                || 'expected promise to throw an error, but it didn\'t'
            const error = {
              showDiff: false,
            }
            throw new AssertionError(message, error, utils.flag(this, 'ssfi'))
          }
          else {
            return
          }
        }
        _super.apply(this, args)
      }
    })
  })

  // @ts-expect-error @internal
  def('withTest', function (test: Test) {
    utils.flag(this, 'vitest-test', test)
    return this
  })

  def('toEqual', function (expected) {
    const actual = utils.flag(this, 'object')

View on GitHub (pinned to d568f8ce37)

Solutions

  1. If you want to assert the promise rejects, use expect(promise).rejects.toThrow() instead of .resolves.toThrow().
  2. If you genuinely expect the resolved value to be a function that throws, ensure the promise resolves to a function, e.g. expect(Promise.resolve(() => { throw new Error() })).resolves.toThrow().
  3. Remove the .resolves modifier and assert on the synchronous throwing function directly: expect(fn).toThrow().

Example fix

// before
await expect(fetchData()).resolves.toThrow();
// after (assert rejection)
await expect(fetchData()).rejects.toThrow();
Defensive patterns

Strategy: validation

Validate before calling

const value = await promise;
if (typeof value !== 'function') throw new TypeError('resolved value is not a function — use .rejects.toThrow() to assert rejection');

Type guard

const resolvesToThrowingFn = async (p: Promise<unknown>): p is Promise<() => never> =>
  typeof (await p.catch(() => null)) === 'function';

Try / catch

try { await expect(promise).resolves.toThrow(); } catch (e) { if (/expected promise to throw/i.test(String(e))) { /* switch to .rejects */ } else throw e; }

Prevention

When it happens

Trigger: Calling expect(promise).resolves.toThrow() or expect(promise).resolves.toThrow('string') or expect(promise).resolves.toThrow(/regex/) where the promise resolves to a non-function value (e.g. a number, object, or string). The code path is entered via the chai 'throws' overwrite (promise==='resolves' && typeof object !== 'function' && !isNot).

Common situations: Developers mistakenly use .resolves.toThrow() expecting it to verify that the promise itself rejects, when in fact .rejects.toThrow() is required. Also happens when refactoring a synchronous expect(fn).toThrow() into an async version and forgetting that the resolved value must be a throwing function.

Related errors


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