Skip to content

@unthrown/vitest: capture a stack at assertion time so forgotten-await failures point at the call site #192

Description

@btravers

When a test forgets to await an async assertion, failOnForgottenAwait
(packages/vitest/src/index.ts:207) throws:

@unthrown/vitest: 1 async assertion(s) (toBeOk) were still pending when the test
ended — a forgotten `await`. For an AsyncResult the matcher is asynchronous:
write `await expect(asyncResult).toBeOk()`.

It names the matcher and (via the failing test) the test, but not the line. In a
long spec with several toBeOk assertions — or under test.concurrent, where
the mechanism already has to disambiguate by test name — finding the one missing
await is a manual scan.

Proposal

settle() already builds an InFlight entry on the async path
(index.ts:124). Capture a stack there and carry it:

type InFlight = {
  matcherName: string;
  testName: string | undefined;
  abandon: () => void;
  callSite: Error; // new
};

Construct it right where the gate is created — the frame directly above is the
user's expect(...) call — and in failOnForgottenAwait attach it to the thrown
error, either as cause or by appending the first non-library frame to the
message.

Why it is cheap

  • The Error is only constructed on the async path, which is exactly the
    path that can be forgotten. The sync path (index.ts:118, a plain Result)
    never touches it.
  • V8 stack formatting is lazy; .stack is only read when the hook actually
    fires. A correctly-awaited assertion pays for the capture and nothing else.

Points to settle in review

  • cause vs. inlining a frame into the message — cause is cleaner but some
    reporters bury it.
  • Whether to trim library frames before reporting, so the top frame is the user's
    expect(...) rather than settle.
  • The existing tests for the mechanism live alongside failOnForgottenAwait's
    exported entry point, so this stays directly testable without a real forgotten
    await.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions