fix: guard async onTrace callbacks against unhandled rejection

Detect when onTrace returns a Promise and .catch() the rejection,
preventing unhandled promise rejection from crashing the process.
Addresses Codex review feedback on PR #40.
This commit is contained in:
JackChen 2026-04-03 15:13:35 +08:00
parent a49b24c22a
commit 8f7fc2019b
2 changed files with 27 additions and 1 deletions

View File

@ -15,12 +15,19 @@ export function emitTrace(
): void {
if (!fn) return
try {
fn(event)
// Guard async callbacks: if fn returns a Promise, swallow its rejection
// so an async onTrace never produces an unhandled promise rejection.
const result = fn(event) as unknown
if (result && typeof (result as Promise<unknown>).catch === 'function') {
;(result as Promise<unknown>).catch(noop)
}
} catch {
// Intentionally swallowed — observability must never break execution.
}
}
function noop() {}
/** Generate a unique run ID for trace correlation. */
export function generateRunId(): string {
return randomUUID()

View File

@ -136,6 +136,25 @@ describe('emitTrace', () => {
}),
).not.toThrow()
})
it('swallows rejected promises from async callbacks', async () => {
// An async onTrace that rejects should not produce unhandled rejection
const fn = async () => { throw new Error('async boom') }
emitTrace(fn as unknown as (event: TraceEvent) => void, {
type: 'agent',
runId: 'r1',
agent: 'a',
turns: 1,
tokens: { input_tokens: 0, output_tokens: 0 },
toolCalls: 0,
startMs: 0,
endMs: 0,
durationMs: 0,
})
// If the rejection is not caught, vitest will fail with unhandled rejection.
// Give the microtask queue a tick to surface any unhandled rejection.
await new Promise(resolve => setTimeout(resolve, 10))
})
})
describe('generateRunId', () => {