/fixes #9877 /claim #9877

Context

A Promise awaiting completion is essentially a Fiber parked awaiting an async callback. When a Fiber forks work that will eventually complete a promise, then awaits that promise, we end up with an extra fiber + flatMap for the bridging:

fiber.await.flatMap(exit => promise.done(exit)).fork *> promise.await

This creates unnecessary allocations and indirection — the promise and fiber are doing the same job independently.

Approach

Promise.become(fiber) wires a fiber’s completion directly to a promise at the unsafe level using fiber.unsafe.addObserver. The observer callback calls completeWith on the promise when the fiber exits, so there’s no intermediate ZIO value or extra fiber involved:

// Before (traditional pattern):
fiber.await.flatMap(exit => promise.done(exit)).fork *> promise.await
// After:
promise.become(fiber) *> promise.await

The implementation is ~20 lines in Promise.scala:

  • Safe API: def become(fiber: Fiber.Runtime[E, A]): UIO[Boolean] — returns whether the link was established (promise was still pending)
  • Unsafe impl: checks if the promise is Done (returns false), otherwise attaches the observer (returns true)

Benchmarks

JMH benchmark comparing the full pattern (create promise → fork fiber → bridge → await) repeated n times:

Benchmark (n) Mode Cnt Score Error Units
PromiseBecomeBenchmark.promiseBecomeForkAwait 1000 thrpt 5 1863.917 ± 199.008 ops/s
PromiseBecomeBenchmark.promiseBecomeForkAwait 10000 thrpt 5 192.390 ± 43.869 ops/s
PromiseBecomeBenchmark.promiseBecomeForkAwait 100000 thrpt 5 14.736 ± 0.898 ops/s
PromiseBecomeBenchmark.traditionalForkAwaitPromise 1000 thrpt 5 681.232 ± 377.856 ops/s
PromiseBecomeBenchmark.traditionalForkAwaitPromise 10000 thrpt 5 96.271 ± 32.285 ops/s
PromiseBecomeBenchmark.traditionalForkAwaitPromise 100000 thrpt 5 4.774 ± 4.860 ops/s
n Traditional Promise.become Improvement
1,000 681 ops/s 1,864 ops/s +174%
10,000 96 ops/s 192 ops/s +100%
100,000 4.8 ops/s 14.7 ops/s +209%

The improvement holds (and actually grows) under load because become avoids creating the bridging fiber entirely.

To reproduce: sbt 'benchmarks/Jmh/run PromiseBecomeBenchmark'

Tests

Four cases added to PromiseSpec:

  • become completes promise with fiber success — happy path
  • become completes promise with fiber failure — error propagation
  • become returns false for already completed promise — idempotency
  • become with already completed fiber — eager completion

Files changed

  • core/shared/src/main/scala/zio/Promise.scalabecome + unsafe impl
  • core-tests/shared/src/test/scala/zio/PromiseSpec.scala — 4 tests
  • benchmarks/src/main/scala/zio/PromiseBecomeBenchmark.scala — JMH benchmark

Claim

Total prize pool $750
Total paid $0
Status Pending
Submitted February 20, 2026
Last updated February 20, 2026

Contributors

MI

Miguel Lemos

@miguelemosreverte

100%

Sponsors

ZI

ZIO

@ZIO

$750