Feature comparison

Side-by-side comparison

The fundamental difference: setTimeout produces a degenerate distribution with a single value. slowdep produces a lognormal distribution that matches how real production dependencies behave.

featuresetTimeoutslowdep
delay distribution fixed — always exactly N ms lognormal — fast most of the time, long tail
p50 accuracy set a fixed delay near p50 p50 is a first-class parameter
p99 realism cannot model tail behavior p99 is a first-class parameter
variance across calls zero — all calls identical natural variance from distribution
error injection manual — throw inside the promise errorRate option — probabilistic errors
TypeScript support native full — preserves generics
preserves function signature requires manual wrapping automatic — same args, same return type
zero setup built in npm install slowdep
realistic for retry testing no — retries fire at exactly the threshold yes — natural distribution exercises retry paths
realistic for timeout testing no — timeouts always fire or never fire yes — some calls exceed timeout, most don't
Code comparison

The same test, written two ways

Here's an integration test for a user service. Written with setTimeout first, then with slowdep. The tests look similar, but they catch fundamentally different bugs.

// with setTimeout — looks fine, hides bugs
describe('UserService', () => {
  const mockDb = {
    findUser: jest.fn(async (id: string) => {
      await new Promise(r => setTimeout(r, 200)); // always 200ms
      return { id, name: 'Alice' };
    }),
  };

  it('fetches a user', async () => {
    const user = await new UserService(mockDb).getUser('1');
    expect(user.name).toBe('Alice');
  });

  // bugs this test CANNOT catch:
  // ? Timeout of 250ms fires when DB takes 300ms — never happens here
  // ? Retry fires when DB is slow — always exactly 200ms, retry never triggers
  // ? Circuit breaker trips on 30% error rate — no errors injected
  // ? Connection pool exhausted by 1% slow calls — no variance
});
// with slowdep — catches real latency bugs
import { withLatency } from 'slowdep';

describe('UserService', () => {
  const mockDb = {
    findUser: withLatency(
      async (id: string) => ({ id, name: 'Alice' }),
      { p50: 5, p99: 200, errorRate: 0.02 } // 2% error rate
    ),
  };

  it('fetches a user under realistic latency', async () => {
    const user = await new UserService(mockDb).getUser('1');
    expect(user.name).toBe('Alice');
  });

  // bugs this test CAN catch:
  // ? Timeout of 250ms fires — some calls will take 200ms+, timeout may trigger
  // ? Retry fires on errors — 2% errorRate means retries are exercised
  // ? Circuit breaker trips — combine with higher errorRate to test threshold
  // ? P99 behavior — naturally sampled, loading states tested realistically
});
Bug classes

What each approach catches

The difference isn't cosmetic. Each approach finds a different class of bugs. Tests written with setTimeout give you false confidence about code that will break under realistic production latency.

setTimeout catches
Functional correctness
Does the function return the right value? Is the response parsed correctly? Does error handling fire on explicit errors? These don't depend on latency distribution.
slowdep catches
Latency-dependent behavior
Does retry logic fire at the right threshold? Is the timeout calibrated correctly? Does the circuit breaker trip when it should? Does the loading state render for the right duration?
setTimeout misses
Tail latency bugs
Bugs that only appear in the 1% of calls that take 10× longer than normal. These bugs are invisible in tests with a fixed delay but cause production incidents at scale.
use both
Combined strategy
Use Jest mocks for unit tests (functional correctness, isolation). Use slowdep for integration tests (latency behavior, retry, timeouts). They're complementary.
Migration

Migrating from setTimeout

The migration is mechanical. Find every await new Promise(r => setTimeout(r, N)) that wraps a dependency call, and replace it with withLatency.

// before
const mockQuery = jest.fn(async (sql) => {
  await new Promise(r => setTimeout(r, 200));
  return FIXTURE_DATA;
});

// after — one import, one wrap
import { withLatency } from 'slowdep';

const mockQuery = withLatency(
  jest.fn(async (sql) => FIXTURE_DATA),
  { p50: 5, p99: 200 }  // the 200ms you had before is now your p99
);
The 200ms heuristic

If you had setTimeout(r, 200), your intent was probably "simulate a slow DB call." Use that 200ms as your p99 and pick a realistic p50 (for Postgres, that's 5ms). The result is far more representative.