Skip to content

fix: async event listeners silently drop Promises #136

@coji

Description

@coji

Problem

Event listeners are typed as (event) => void, but TypeScript allows async functions in void-returning callbacks. When a user (or our own code) passes an async listener:

  1. The returned Promise is silently ignored — no await, no .catch()
  2. Rejected Promises never reach onError, becoming unhandled rejections
  3. Users get no warning that their async work isn't being awaited

Our own code has this bug

withLogPersistence() registers an async listener on log:write:

durably.on('log:write', async (event) => {
  await durably.storage.createLog({...})  // Promise silently dropped
})

This means log persistence is fire-and-forget with no error handling — not the intended behavior.

Proposed Fix

Based on Codex review feedback:

  1. Detect Promise-returning listeners at runtime — if a listener returns a thenable, attach .catch() to forward rejections to onError (but never await)
  2. Add dev-time warning when a listener returns a Promise (e.g., console.warn in non-production)
  3. Fix withLogPersistence() — either make log persistence a first-class internal path, or move it to a proper async mechanism that doesn't rely on on()

Design Constraints

  • emit() must stay synchronous — async emit pushes user callback latency into the worker hot path
  • on() contract is observational (sync, non-blocking) — this should not change
  • If awaited async hooks are needed in the future, add a separate hook() API with explicit semantics

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions