Skip to content

Fix: Jest Async Test Timeout — Exceeded 5000ms or Test Never Resolves

FixDevs ·

Quick Answer

How to fix Jest async test timeouts — missing await, unresolved Promises, done callback misuse, global timeout configuration, fake timers, and async setup/teardown issues.

The Problem

A Jest test times out instead of failing or passing:

● UserService › fetchUser › returns user data

  Timeout - Async callback was not invoked within the 5000ms timeout specified by jest.setTimeout.
  Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.

Or a test hangs indefinitely — Jest process never exits:

jest --testPathPattern=user.test.ts
# Tests pass but Jest process hangs — never returns to shell prompt

Or a Promise-based test always passes even when the assertion inside is false:

test('should reject with error', () => {
  fetchUser(-1).catch(err => {
    expect(err.message).toBe('User not found');
    // This assertion never runs — test passes vacuously
  });
  // Promise returned to .catch but test doesn't wait for it
});

Why This Happens

Jest’s async handling depends on the test function correctly signaling completion. Common failures:

  • Missing await — a Promise is created but not awaited. The test function returns immediately (before the async work finishes), and Jest marks it as passed without checking assertions.
  • done callback never called — using the done pattern but not calling done() (or done(error)) in all code paths. Jest waits for done forever.
  • Unresolved Promise chain — a .then() or .catch() that silently swallows errors leaves a Promise pending. Jest waits but the Promise never settles.
  • Open handles — a database connection, timer, or network socket opened during the test isn’t closed. Jest waits for all handles to close before exiting.
  • beforeAll/afterAll not completing — if setup or teardown is async and doesn’t resolve, no tests in the suite run (they all timeout).
  • Default timeout too low — complex integration tests legitimately take more than 5 seconds. The global or per-test timeout needs increasing.

Fix 1: Add Missing await

The most common cause — async test functions without proper awaiting:

// WRONG — no async, Promise returned but not awaited
test('fetches user', () => {
  fetchUser(1).then(user => {
    expect(user.name).toBe('Alice');  // Assertion runs after test ends
  });
  // Test returns immediately — always "passes" (no assertions run)
});

// WRONG — async but missing await in the chain
test('fetches user', async () => {
  const result = fetchUser(1);   // Missing await — result is a Promise, not user
  expect(result.name).toBe('Alice');   // Error: Cannot read .name of Promise
});

// CORRECT — await the Promise
test('fetches user', async () => {
  const user = await fetchUser(1);   // Waits for the Promise to resolve
  expect(user.name).toBe('Alice');   // Runs after user is available
});

// CORRECT — return the Promise (works without async/await)
test('fetches user', () => {
  return fetchUser(1).then(user => {
    expect(user.name).toBe('Alice');
  });
  // Returning the Promise tells Jest to wait for it
});

Common missing await locations:

// WRONG — beforeEach not awaited
beforeEach(() => {
  db.connect();   // Missing await — tests run before DB connects
});

// CORRECT
beforeEach(async () => {
  await db.connect();
});

// WRONG — assertion inside .then not returned
test('updates user', async () => {
  await updateUser(1, { name: 'Bob' });
  fetchUser(1).then(user => {
    expect(user.name).toBe('Bob');  // Not returned — may not run
  });
});

// CORRECT
test('updates user', async () => {
  await updateUser(1, { name: 'Bob' });
  const user = await fetchUser(1);
  expect(user.name).toBe('Bob');
});

Fix 2: Use done Callback Correctly

When using the done callback pattern, call it in ALL code paths:

// WRONG — done never called if fetchUser throws
test('fetches user', (done) => {
  fetchUser(1).then(user => {
    expect(user.name).toBe('Alice');
    done();
  });
  // If fetchUser rejects, done is never called → timeout
});

// CORRECT — call done in error cases too
test('fetches user', (done) => {
  fetchUser(1)
    .then(user => {
      expect(user.name).toBe('Alice');
      done();
    })
    .catch(error => {
      done(error);  // Call done with error to fail the test cleanly
    });
});

// BETTER — use async/await instead of done for clarity
test('fetches user', async () => {
  const user = await fetchUser(1);
  expect(user.name).toBe('Alice');
});

done with callbacks (not Promises):

// For callback-based APIs — done is appropriate
test('reads file', (done) => {
  fs.readFile('./data.json', 'utf8', (err, data) => {
    if (err) return done(err);  // Fail test with error

    const parsed = JSON.parse(data);
    expect(parsed.version).toBe('1.0');
    done();  // Signal completion
  });
});

Fix 3: Test Rejected Promises

Testing that a function rejects requires careful handling:

// WRONG — assertion in .catch not returned
test('rejects invalid user', () => {
  fetchUser(-1).catch(err => {
    expect(err.message).toBe('Not found');
    // Not returned — test passes even if assertion fails
  });
});

// CORRECT — use expect(...).rejects
test('rejects invalid user', async () => {
  await expect(fetchUser(-1)).rejects.toThrow('Not found');
  // Or: await expect(fetchUser(-1)).rejects.toMatchObject({ message: 'Not found' });
});

// CORRECT — alternatively, handle manually
test('rejects invalid user', async () => {
  expect.assertions(1);  // Ensures at least 1 assertion runs

  try {
    await fetchUser(-1);
    fail('Expected fetchUser to throw');  // Shouldn't reach here
  } catch (err) {
    expect(err.message).toBe('Not found');
  }
});

expect.assertions(n) — guard against missing assertions:

test('async operations all run', async () => {
  expect.assertions(3);  // Test fails if exactly 3 assertions don't run

  const user = await fetchUser(1);
  expect(user).toBeDefined();       // Assertion 1
  expect(user.name).toBe('Alice');  // Assertion 2
  expect(user.email).toContain('@'); // Assertion 3
  // If any assertion is skipped (e.g., due to early return), test fails
});

Fix 4: Increase Timeout for Slow Tests

When tests legitimately take longer than 5 seconds:

// Per-test timeout (milliseconds)
test('slow integration test', async () => {
  const result = await slowDatabaseOperation();
  expect(result).toBeDefined();
}, 30000);  // 30 second timeout for this test

// Per-describe timeout (applies to all tests in the block)
describe('integration tests', () => {
  beforeAll(async () => {
    await startDatabase();
  }, 30000);  // 30s for beforeAll

  test('complex query', async () => {
    const data = await runComplexQuery();
    expect(data.length).toBeGreaterThan(0);
  }, 15000);  // 15s for this test
});

// Global timeout — applies to all tests in the file
// jest.config.js
module.exports = {
  testTimeout: 15000,  // 15 seconds globally
};

// Or per-file using jest.setTimeout
// At the top of the test file:
jest.setTimeout(30000);  // Applies to all tests in this file

Fix 5: Fix Open Handles

Jest warns about open handles that prevent clean exit:

# Jest output:
# Jest did not exit one second after the test run has completed.
# This usually means that there are asynchronous operations that weren't stopped in your tests.
# Consider running Jest with `--detectOpenHandles` to troubleshoot this issue.

jest --detectOpenHandles
# Output shows which handles are open (database connections, timers, servers)

Common open handles and how to fix them:

// Database connection not closed
describe('UserService', () => {
  let connection;

  beforeAll(async () => {
    connection = await createDatabaseConnection();
  });

  afterAll(async () => {
    await connection.close();   // MUST close after tests
  });

  test('finds user', async () => {
    const user = await connection.query('SELECT * FROM users WHERE id = 1');
    expect(user).toBeDefined();
  });
});

// HTTP server not closed
let server;

beforeAll(done => {
  server = app.listen(3001, done);
});

afterAll(done => {
  server.close(done);  // Close server after tests
});

// setInterval or setTimeout not cleared
let intervalId;

beforeAll(() => {
  intervalId = setInterval(pollService, 1000);
});

afterAll(() => {
  clearInterval(intervalId);  // Stop the interval
});

Force Jest to exit after tests complete:

# Force exit after tests (workaround — fix the actual handle instead)
jest --forceExit

# Or in jest.config.js:
module.exports = {
  forceExit: true,  // Not recommended hides the real issue
};

Fix 6: Fix async beforeAll and afterAll

Setup and teardown must properly resolve for tests to run:

// WRONG — async beforeAll not awaited correctly
beforeAll(() => {
  setupDatabase();   // Returns Promise but not returned
  // Jest doesn't wait — tests run before DB is set up
});

// CORRECT — return the Promise
beforeAll(() => {
  return setupDatabase();   // Jest waits for this Promise
});

// CORRECT — async/await
beforeAll(async () => {
  await setupDatabase();
  await seedTestData();
  await startMockServer();
});

// CORRECT — done callback
beforeAll(done => {
  setupDatabase()
    .then(() => done())
    .catch(done);
});

// afterAll — must clean up or remaining tests may timeout
afterAll(async () => {
  await db.clearTestData();
  await db.disconnect();
  mockServer.stop();
});

Fix 7: Fake Timers with Async Tests

Combining jest.useFakeTimers() with async tests requires special handling:

// WRONG — fake timers prevent Promise resolution in some setups
jest.useFakeTimers();

test('debounced function calls', async () => {
  const fn = jest.fn();
  const debouncedFn = debounce(fn, 500);

  debouncedFn();
  jest.advanceTimersByTime(500);

  // If your async code uses setTimeout internally,
  // Promises may not resolve with fake timers
  await waitFor(() => expect(fn).toHaveBeenCalled());
  // Can hang if waitFor uses real time intervals
});

// CORRECT — use runAllTimers or specific advancement
jest.useFakeTimers();

test('debounced function calls', () => {
  const fn = jest.fn();
  const debouncedFn = debounce(fn, 500);

  debouncedFn();
  jest.runAllTimers();  // Executes all pending timers immediately

  expect(fn).toHaveBeenCalledTimes(1);
});

// For tests mixing real Promises and fake timers:
test('async with fake timers', async () => {
  const result = someAsyncOperation();  // Returns Promise

  // Advance timers to unblock any setTimeout inside someAsyncOperation
  jest.runAllTimersAsync();  // Jest 29+ — runs timers and flushes Promises

  expect(await result).toBeDefined();
});

Still Not Working?

jest --runInBand — run tests serially (one at a time) to isolate which test is timing out:

jest --runInBand --testPathPattern=problem.test.ts

Circular Promises — a Promise that depends on itself or creates a cycle never resolves. Add console.log statements at key points to trace where execution stops.

Module mocking hiding async errors — if a mocked module’s async function resolves immediately but the real implementation has async delays, the test may work in unit tests but fail in integration. Verify that mocks accurately represent real async timing.

--testTimeout=60000 flag — use a very high timeout temporarily to confirm the test can pass (just slowly), vs never passing at all:

jest --testTimeout=60000 problem.test.ts

For related testing issues, see Fix: Jest Fake Timers Not Working and Fix: Jest Coverage Not Collected.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles