Skip to content

Vitest False Positive in PR Added in-memory locks for the process, fix for Bug #2579 temporary measure. #3060 #3073

Closed

Description

@davidw-philips

Describe the bug

Vitest reports an “Unhandled Errors” warning for a test that intentionally throws an error ("should throw error for non-existent entity").
The test itself passes correctly using .rejects.toThrow(), but Vitest incorrectly surfaces the expected rejection as an unhandled error.

This causes the TypeScript / Test memory CI job to fail even though there is no bug in the memory server implementation.

To Reproduce

Steps to reproduce the behavior:

Run the memory server tests using:

pnpm test:memory

Look for the test:

"should throw error for non-existent entity"

Observe that the test passes but Vitest outputs an “Unhandled Errors” warning.

In GitHub Actions, this warning results in a failing test job.

Expected behavior

Vitest should correctly treat .rejects.toThrow() as a handled and expected error, and should not report the intentionally thrown error as an unhandled rejection.

CI should pass when the test succeeds, without false-positive warnings.

Logs

Example output:

Unhandled Errors:
❯ Error: Entity does not exist
at ...

The test reports PASS

The warning appears only in the "Unhandled Errors" summary

No runtime errors or implementation failures occur

Additional context

This behavior is a Vitest testing quirk, not a bug in the memory server code.

All locking, file serialization, and race condition tests pass successfully.

The new lock implementation is functioning as expected.

The false warning appears because Vitest sometimes logs expected async errors as “unhandled” even when wrapped in .rejects.toThrow().

Possible mitigations (optional):

Rewrite the test using assert.rejects(...)

Wrap error tests to avoid Vitest’s unhandled rejection reporting

Add scoped process.on("unhandledRejection", () => {}); for this file (not recommended except for CI cleanliness)

If you want, I can also rewrite the test file for a clean CI run.

Activity

  1. sstklen commented on Feb 25, 2026

    @sstklen

    Hey! I ran into a similar pattern in our bug knowledge base and thought this might help.

    What's happening: Vitest's global unhandled rejection handler captures the Promise rejection before .rejects.toThrow() can assert on it. This happens because the async function throws synchronously inside the Promise executor, and Vitest's error tracking hooks see the rejection before the test framework's .rejects wrapper can intercept it. The in-memory lock PR (#3060) added a test that calls a method expected to throw for a non-existent entity, but the thrown error propagates to Node's unhandledRejection event before Vitest's .rejects.toThrow() matcher resolves.

    What worked for us:

    Wrap the expected-to-throw call in a try/catch block inside the test instead of relying on .rejects.toThrow(), or use expect(async () => { await call(); }).rejects.toThrow() with an explicit await on the outer expect. Alternatively, if the method under test throws synchronously (not returning a rejected Promise), use expect(() => call()).toThrow() without .rejects. Another approach is to add a Vitest config option to suppress unhandled rejection warnings for known test patterns.

    Steps:

    1. Identify the failing test in the memory server test file (likely src/memory/tests or similar path) that uses .rejects.toThrow() for the 'non-existent entity' case
    2. Option A: Change from expect(promise).rejects.toThrow() to await expect(async () => { await methodCall(); }).rejects.toThrow('Entity does not exist') — ensuring the outer expect is properly awaited
    3. Option B: Wrap in try/catch: try { await methodCall(); expect.unreachable(); } catch (e) { expect(e.message).toBe('Entity does not exist'); }
    4. Option C: If the function throws synchronously before returning a Promise, use expect(() => methodCall()).toThrow() without .rejects
    5. Verify CI passes by running pnpm test:memory locally and confirming no 'Unhandled Errors' warning appears
    // Before (causes Vitest false positive unhandled rejection):
    it('should throw error for non-existent entity', async () => {
      await expect(server.getEntity('nonexistent')).rejects.toThrow('Entity does not exist');
    });
    
    // After (Option A - properly awaited async closure):
    it('should throw error for non-existent entity', async () => {
      await expect(async () => {
        await server.getEntity('nonexistent');
      }).rejects.toThrow('Entity does not exist');
    });
    
    // After (Option B - try/catch, most reliable):
    it('should throw error for non-existent entity', async () => {
      try {
        await server.getEntity('nonexistent');
        expect.unreachable('Should have thrown');
      } catch (e) {
        expect((e as Error).message).toBe('Entity does not exist');
      }
    });
    

    📊 We found 1 similar case in our knowledge base with the same pattern — this gives us medium confidence in this analysis.

    Hope this helps! Let me know if it doesn't match your case — happy to dig deeper. 🦞

    Disclosure: This analysis is from Confucius Debug, an AI-powered community KB for agent bugs. Please verify before applying.


    🦞 Confucius Debug — community knowledge base for AI agent bugs. Free to search via MCP.

  2. added a commit that references this issue on Apr 10, 2026
    fd782a8
  3. added a commit that references this issue on Sep 2, 2026
    cce08de
  4. olaservo commented on Sep 2, 2026

    @olaservo
    Member

    Fixed by #3893, which wraps the assertion in an async closure so the expected rejection is scoped to the expect chain. Closing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions