measure-fn
v3.11.0
Published
Zero-dependency function performance measurement with hierarchical logging
Maintainers
Readme
Whenever a function needs error handling so it doesn't crash, and timing so you know how long it took, you usually end up adding this boilerplate manually:
Before:
let users = null;
try {
const start = performance.now();
users = await fetchUsers();
const ms = (performance.now() - start).toFixed(2);
console.log(`[a] ··········· ${ms}ms → ${JSON.stringify(users)}`);
} catch (e) {
console.log(`[a] ✗ Fetch users (${e.message})`);
console.error(e.stack);
}After: measure-fn does the exact same thing in one line. Completely type-safe (infers T | null) and never crashes.
import { measure } from 'measure-fn';
const users = await measure('Fetch users', () => fetchUsers());
// → [a] ··········· 86ms → [{"id":1},{"id":2}]Installation
npm install measure-fn
# or bun add / pnpm add / yarn add✨ Defaults
Every measure call automatically:
- 🛡️ Catches errors → logs
✗with a stack trace and returnsnull(no unhandled rejections) - ⏱️ Logs timing → prints
label Nms → resultusingperformance.now() - 🌳 Assigns a trace ID →
[a],[b],[a-a]for zero-config nested hierarchy
🌳 Nested Calls (Tracing)
Pass a child m function to get hierarchical APM-like tracing for free:
await measure('Pipeline', async (m) => {
const user = await m('Fetch user', () => fetchUser(1));
const posts = await m('Fetch posts', () => fetchPosts(user.id));
return posts;
});[a] ... Pipeline
[a-a] ·········· 82ms → {"id":1}
[a-b] ··········· 45ms → [...]
[a] ········ 128msParallel execution works cleanly too:
await measure('Load all', async (m) => {
const [users, posts] = await Promise.all([
m('Users', () => fetchUsers()),
m('Posts', () => fetchPosts()),
]);
});🛡️ Error Handling
By default, errors return null so your pipelines can continue safely:
const user = await measure('Fetch user', () => fetchUser(1));
// If it throws → logs ✗, user = nullCustom Fallbacks: Pass onError as the 3rd argument:
const user = await measure('Fetch user', () => fetchUser(1),
(error) => defaultUser
);
// If it throws → logs ✗, user = defaultUserIf the onError fallback itself throws, that's also safely caught and returns null. measure never crashes.
Fail-Fast (.assert): Use .assert() when you need a guaranteed non-null result:
const user = await measure.assert('Get user', () => fetchUser(1));
// If it throws → logs ✗, re-throws with .cause = original error| Pattern | On error | Return Type |
|---------|----------|-------------|
| measure(label, fn) | returns null | T \| null |
| measure(label, fn, onError) | returns onError(error) | T |
| measure.assert(label, fn) | throws with .cause | T |
🚦 Timeouts & Budgets
The first argument can be a label string, or an options object:
| Field | Type | Effect |
|-------|------|--------|
| label | string | Display name (required if object) |
| timeout | number | Aborts after N ms (returns null) |
| budget | number | Warns if slower than N ms (doesn't abort) |
| maxResultLength | number | Override result truncation (0 = unlimited, inherits to children) |
| any other | any | Logged inline as context metadata |
Timeout (enforce):
const data = await measure({ label: 'Slow API', timeout: 5000 }, () => fetchSlowApi());
// > 5s → ✗ Slow API 5.0s (Timeout (5.0s)), returns nullWorks with onError fallback too.
Budget (warn):
await measure({ label: 'DB query', budget: 100 }, () => db.query('...'));
// → [a] ········ 245ms → [...] ⚠ OVER BUDGET (100ms)Combine both — budget warns early, timeout enforces a hard stop:
await measure({ label: 'Query', budget: 100, timeout: 5000 }, () => query());Metadata context:
await measure({ label: 'Fetch user', userId: 1 }, () => fetchUser(1));
// → [a] ... Fetch user (userId=1)🧰 Extensions
measure.wrap(label, fn)
Wrap a function once, measure every time it's called:
const getUser = measure.wrap('Get user', fetchUser);
await getUser(1); // → [a] ········ 82ms
await getUser(2); // → [b] ········ 75msmeasure.batch(label, items, fn, opts?)
Process arrays with built-in progress logs:
const results = await measure.batch('Process', userIds, async (id) => {
return await processUser(id);
}, { every: 100 });
// → [a] ... Process (500 items)
// → [a] = 100/500 (1.2s, 83/s)
// → [a] ················· 5.3s → "500/500 ok"measure.retry(label, opts, fn)
Automatic retries with delay and backoff:
const result = await measure.retry('Flaky API', {
attempts: 3, delay: 1000, backoff: 2
}, () => fetchFlakyApi());
// → [a] ✗ Flaky API [1/3] 102ms (timeout)
// → [b] ················· 89ms → {"status":"ok"}measure.timed(label, fn?)
Get duration programmatically alongside the result:
const { result, duration } = await measure.timed('Fetch', () => fetchUsers());createMeasure(prefix)
Scoped instances with custom prefixes:
const api = createMeasure('api');
const db = createMeasure('db');
await api.measure('GET /users', async () => {
return await db.measure('SELECT', () => query('...'));
});
// → [api:a] ... GET /users
// → [db:a] ······ 44ms
// → [api:a] ·········· 45msAnnotations & Sync
import { measureSync } from 'measure-fn';
const config = measureSync('Parse config', () => JSON.parse(raw));
await measure('Server ready');
// → [a] = Server ready⚙️ Configuration
import { configure } from 'measure-fn';
configure({
silent: true, // suppress all output
timestamps: true, // prepend [HH:MM:SS.mmm]
maxResultLength: 200, // truncate results (default: 0 = unlimited)
dotEndLabel: false, // show full label on end lines (default: true = dots)
dotChar: '.', // character for dot fill (default: '·')
logger: (event) => { // custom event handler
myTelemetry.track(event);
}
});Env vars: MEASURE_SILENT=1, MEASURE_TIMESTAMPS=1
Output Format
| Symbol | Meaning | Example |
|--------|---------|---------|
| ... | Started | [a] ... Fetch users |
| ··· | Success | [a] ··········· 86ms → [...] |
| ✗ | Error | [a] ✗ ··········· (Network Error) |
| = | Annotation | [a] = Server ready |
IDs encode hierarchy: [a] → root, [a-a] → first child, [a-b] → second child.
License
MIT
