This project has been succeeded by GraphReFly. New development happens at graphrefly-ts. npm install @graphrefly/graphrefly
Skip to content

March 25, 2026

Promises Are the New Callback Hell

Chronicle 22 - Arc 8: Engineering Deep Cuts (Companion)

Promises fixed callback pyramids. async/await fixed Promise chains. So why did we remove Promise-based internals from callbag-recharge?

Because a callbag engine needs a single composition model. Every await in internals created a seam where graph-level cancellation, control signals, and observability stopped.

The rule we adopted

Wrap async at the boundary, stay callbag-native inside.

  • External Promise APIs: adapt with rawFromPromise
  • Async iterables: adapt with rawFromAsyncIter
  • User callbacks of unknown shape: normalize with rawFromAny
  • Internal control flow: continue with callbag subscription paths

This keeps data and lifecycle in one protocol from end to end.

Replacement patterns that mattered most

  • Delay: await new Promise(setTimeout) -> fromTimer(ms)
  • Timeout race: Promise.race(...) -> rawRace(...)
  • Wait-for-condition: await firstValueFrom(...) -> guarded subscribe(...)
  • Callback returns: await cb(...) -> rawFromAny(cb(...))

The changes are mechanical, but they remove opaque control boundaries.

Bugs this refactor revealed

Moving away from Promise deferral exposed real issues:

  • synchronous emission temporal-dead-zone handling
  • missing teardown paths on cancellation
  • "clean END without DATA" adapter edge cases

Those bugs already existed. Promise boundaries were hiding them.

Why this now sits with Arc 8

Promise elimination was not just a style preference. It followed the same correctness theme:

  • one internal protocol
  • explicit lifecycle propagation
  • no hidden scheduler jumps

The result is a system that is easier to reason about under stress and easier to compose without semantic surprises.

Released under the MIT License.