Latency not applied
The most common issue: the original, unwrapped function is still being called instead of the wrapped version. withLatency returns a new function — you must call that new function.
Wrapping the function but continuing to call the original.
import { withLatency } from 'slowdep' // This wraps queryUser but the wrapped version is never used withLatency(queryUser, 'postgres') // Still calling the original — no latency applied const user = await queryUser(id)
Assign the return value of withLatency and call that.
import { withLatency } from 'slowdep' // Assign the wrapped function const slowQuery = withLatency(queryUser, 'postgres') // Call the wrapped version const user = await slowQuery(id)
Lost this context
When wrapping a class method, the wrapped function may lose its this binding. This happens because extracting a method from a class instance severs the connection to the instance. Use .bind() or an arrow function wrapper.
Extracting an unbound method loses this.
class UserService { async getUser(id) { return this.db.query(`SELECT * FROM users WHERE id = $1`, [id]) } } const svc = new UserService() // this.db will be undefined inside the wrapped call const slowGetUser = withLatency(svc.getUser, 'postgres')
Bind the method to the instance before wrapping.
const slowGetUser = withLatency(svc.getUser.bind(svc), 'postgres') // Or use an arrow function wrapper const slowGetUser = withLatency( (id) => svc.getUser(id), 'postgres' )
withLatencyAll handles this binding automatically when wrapping an entire object, so you only need to think about this when using withLatency on individual extracted methods.
Wrapped function errors
When errorRate fires, the wrapped function rejects with a plain Error. The message is "Simulated transient error". The underlying function is never called in this case. Your error handling code needs to handle this rejection exactly as it would handle a real transient error.
import { withLatency } from 'slowdep' const slowCharge = withLatency(stripe.charges.create.bind(stripe.charges), { p50: 200, p99: 2000, errorRate: 0.002 }) try { const charge = await slowCharge({ amount: 1000, currency: 'usd' }) processCharge(charge) } catch (err) { // err.message === 'Simulated transient error' when errorRate fires // Handle exactly as you would a real Stripe network error if (err.message === 'Simulated transient error' || err.type === 'StripeConnectionError') { await retry(() => slowCharge({ amount: 1000, currency: 'usd' })) } }
TypeScript issues
withLatency preserves the original function's complete type signature using generics. The return type of the wrapped function is identical to the original. If TypeScript is reporting type errors with slowdep, check the following:
- TypeScript version: slowdep requires TypeScript 4.5 or later for the generic inference to work correctly. Run
tsc --versionto check. - Strict mode: If
"strict": trueis set, make sure your wrapped functions have explicit return types or that TypeScript can infer them. - Explicit type parameter: If inference fails, provide the type parameter explicitly:
withLatency<typeof myFn>(myFn, 'redis') - Importing types: Use
import type { LatencyOptions, LatencyPreset } from 'slowdep'for type-only imports to avoid importing runtime code in type-checking contexts.
// Explicit typing when inference is ambiguous import { withLatency } from 'slowdep' import type { LatencyOptions } from 'slowdep' const opts: LatencyOptions = { p50: 5, p99: 200, errorRate: 0.001 } const slowFn = withLatency<typeof myAsyncFn>(myAsyncFn, opts)
ESM vs CommonJS
slowdep ships both ESM and CommonJS builds. The correct build is selected automatically based on your project's module format. If you see an error like "does not provide an export named 'withLatency'", the wrong build is being loaded.
- Check your package.json: If
"type": "module"is set, Node.js treats.jsfiles as ESM. If it's absent or"commonjs", they're treated as CJS. - Use the right import syntax: ESM uses
import { withLatency } from 'slowdep'. CJS usesconst { withLatency } = require('slowdep'). - Mixed projects: If you have a CJS project but want to use ESM imports, rename files to
.mjsor set"type": "module". Do not mixrequireandimportin the same file. - Bundlers: webpack, Rollup, Vite, and esbuild all handle the exports map automatically. No special configuration is needed.
Deterministic tests
slowdep uses Math.random() internally. Tests that measure timing or depend on consistent latency values will be flaky unless you mock it. Use jest.spyOn to control the random source:
import { withLatency } from 'slowdep' let randomSpy beforeEach(() => { // Lock Math.random so lognormal sampling is deterministic randomSpy = jest.spyOn(Math, 'random').mockReturnValue(0.5) }) afterEach(() => { randomSpy.mockRestore() }) test('returns user data', async () => { const slowQuery = withLatency(mockQueryUser, 'postgres') const user = await slowQuery(1) // Latency is now consistent — test will not be flaky expect(user.id).toBe(1) })
See Deterministic testing for two additional approaches: mocking the whole wrapper and using an environment variable bypass.
Common mistakes
Calling the function before passing it to withLatency gives you a Promise, not a function. withLatency expects a function.
// Wrong: queryUser() is called immediately, returns a Promise const slow = withLatency(queryUser(1), 'postgres') // Correct: pass the function reference, not a call to it const slowQuery = withLatency(queryUser, 'postgres') const result = await slowQuery(1)
slowdep can wrap synchronous functions, but the added delay is still async — the result will be a Promise even if the original function returned a plain value. This is usually not what you want. Prefer wrapping async functions that already return Promises.