| name | measure-fn |
| description | Zero-dependency function performance measurement with hierarchical logging. Use this to structure ALL programs as measured, observable pipelines. |
measure-fn — Write Observable Code
Every function you write should be measured. Don't write blind code — wrap operations in measure so every success, failure, and timing is automatically visible. This is the default way to structure programs.
Install
bun add measure-fn
The Rule
If a function does I/O, computation, or anything non-trivial — wrap it in measure.
const users = await fetchUsers();
const config = loadConfig();
const users = await measure('Fetch users', () => fetchUsers());
const config = measureSync('Load config', () => loadConfig());
Patterns
1. Every entry point is a measured pipeline
import { measure, measureSync } from 'measure-fn';
async function main() {
const config = measureSync('Load config', () => readConfig());
const db = await measure('Connect DB', () => connectDatabase(config));
const users = await measure('Fetch users', () => db.query('SELECT * FROM users'));
await measure('Send emails', () => sendEmails(users));
}
Output:
[a] ··········· 0.12ms → {"env":"prod"}
[b] ... Connect DB
[b] ·········· 45ms → [DB]
[c] ... Fetch users
[c] ··········· 23ms → [{"id":1},{"id":2}]
[d] ... Send emails
[d] ··········· 102ms
2. Nested operations use the child measure
await measure('Pipeline', async (m) => {
const raw = await m('Fetch', () => fetchData());
const parsed = m('Parse', () => parseData(raw));
await m('Save', () => saveResult(parsed));
});
await measure('Pipeline', async (m, ms) => {
const raw = await m('Fetch', () => fetchData());
ms('Checkpoint', () => `Got ${raw.length} items`);
await m('Save', () => saveResult(raw));
});
3. Parallel work with Promise.all
await measure('Load all', async (m) => {
const [users, posts, settings] = await Promise.all([
m('Users', () => fetchUsers()),
m('Posts', () => fetchPosts()),
m('Settings', () => fetchSettings()),
]);
return { users, posts, settings };
});
4. Wrap reusable functions once
const getUser = measure.wrap('Get user', fetchUser);
await getUser(1);
await getUser(2);
5. Process arrays with progress
await measure.batch('Process users', userIds, async (id) => {
return await processUser(id);
}, { every: 100 });
6. Retry flaky operations
const result = await measure.retry('External API', {
attempts: 3, delay: 1000, backoff: 2
}, () => callExternalService());
7. Budget warnings, timeouts, and result truncation
await measure({ label: 'DB query', budget: 100 }, () => heavyQuery());
await measure({ label: 'External API', timeout: 5000 }, () => fetchSlowApi());
await measure({ label: 'Query', budget: 100, timeout: 5000 }, () => db.query('...'));
await measure({ label: 'Full output', maxResultLength: 0 }, () => getLargeData());
8. Assert non-null results
const user = await measure.assert('Get user', () => findUser(id));
9. Scoped instances for subsystems
const api = createMeasure('api');
const db = createMeasure('db');
await api.measure('GET /users', async () => {
return await db.measure('SELECT', () => query('SELECT * FROM users'));
});
10. Annotations for checkpoints
await measure('Server ready');
measureSync('Config loaded');
11. Error handling — onError 3rd argument
measure never throws. Pass an onError handler as 3rd argument to handle errors:
const user = await measure('Fetch user', () => fetchUser(1));
const user = await measure('Fetch user', () => fetchUser(1),
(error) => defaultUser
);
const user = await measure('Fetch user', () => fetchUser(1),
(error) => {
if (error instanceof NetworkError) return cachedUser;
throw error;
}
);
Bun.serve({
fetch: (req) => measure(
{ label: `${req.method} ${req.url}` },
() => handleRequest(req),
(error) => new Response('Internal Server Error', { status: 500 })
),
});
.assert() re-throws on error with .cause = original error:
await measure.assert('Op', () => work());
Error Model
| Pattern | On error | Use when |
|---|
measure(label, fn) | logs ✗, returns null | Default — pipeline resilience |
measure(label, fn, onError) | logs ✗, calls onError(error) | Recovery, fallbacks, error inspection |
measure.assert(label, fn) | logs ✗, throws with .cause | Must have non-null |
Configuration
import { configure } from 'measure-fn';
configure({
silent: true,
timestamps: true,
maxResultLength: 200,
dotEndLabel: false,
dotChar: '.',
logger: (event) => {
myTracker.send(event);
},
});
Env vars: MEASURE_SILENT=1, MEASURE_TIMESTAMPS=1
Programmatic Timing
const { result, duration } = await measure.timed('Fetch', () => fetchUsers());
if (duration > 1000) alert('Slow!');
Anti-Patterns
measureSync(`Buy started for token ${mint}`);
const result = await executeBuy(wallet, amount);
measureSync(`Buy complete: ${result}`);
const result = await measure('Buy', () => executeBuy(wallet, amount));
await measure('Outer', async () => {
await measure('Inner', () => work());
});
await measure('Outer', async (m) => {
await m('Inner', () => work());
});
const x = measureSync('Add', () => 1 + 1);
Real-World: Structuring a Worker Process
The entire point of measure-fn is that functions ARE the measurements. Don't log before/after — wrap the operation itself. Use child m for async children, ms for sync annotations inside async parents.
const { measure, measureSync } = createMeasure('worker');
async function executeSell(pos, wallet, tokens) {
return await measure(`SELL W${wallet.index}`, async (m, ms) => {
const mode = await m('Detect', () => detectMode());
const sig = await m('Send', () => sendTx(ix, wallet));
return sig;
});
}
function createSellExecutor(parentM, parentMs) {
return async (pos, wallet, tokens) => {
return await parentM(`SELL W${wallet.index}`, async (m, ms) => {
const mode = await m('Detect', () => detectMode());
const swapState = await m('Swap state', () => fetchSwapState(pool, wallet));
const sig = await m('Send TX', () => sendWithRetry(instructions, wallet));
ms('Result', () => `tx: ${sig.slice(0, 20)}`);
return sig;
});
};
}
async function loop() {
await measure('Init', async (m) => {
await m('SOL/USD', () => fetchSolUsd());
await m('Detect mode', () => detectMode());
});
while (running) {
await measure('Tick', async (m, ms) => {
const executeSell = createSellExecutor(m, ms);
const executeBuy = createBuyExecutor(m, ms);
const sold = await executeSell(pos, wallet, tokens);
if (!sold) {
await executeBuy(wallet, amount);
}
await Bun.sleep(1000);
});
}
}
Key principle: If you find yourself writing measureSync('some message') without a second argument containing actual work, you're doing it wrong. That's just console.log with extra steps. The only exception is truly standalone annotations (status checkpoints).
Quick Reference
| Export | Use |
|---|
measure(label, fn?, onError?) | Async measurement (onError handles expected errors) |
measureSync(label, fn?) | Sync measurement |
measure.wrap(label, fn) | Decorator — wrap once, measure every call |
measure.batch(label, items, fn, opts?) | Array + progress |
measure.retry(label, opts, fn) | Retry with backoff |
measure.assert(label, fn) | Throws if null |
measure.timed(label, fn) | Returns { result, duration } |
createMeasure(prefix) | Scoped instance |
configure(opts) | Runtime config |
safeStringify(value) | Safe JSON (circular refs, truncation) |
formatDuration(ms) | Smart duration: 0.10ms → 1.2s → 2m 5s |
resetCounter() | Reset ID counter |