Skip to content

fix: fail instead of throw when a transform on a completed FastFuture throws - #1305

Merged
pjfanning merged 1 commit into
apache:mainfrom
pjfanning:fastfuture-transform-exceptions
Sep 17, 2026
Merged

pjfanning merged 1 commit into
apache:mainfrom
pjfanning:fastfuture-transform-exceptions

Conversation

@pjfanning

Copy link
Copy Markdown
Member

Motivation

FastFuture.successful / FastFuture.failed return already completed futures that override Future.transform to apply the function directly instead of scheduling it. Unlike DefaultPromise, that override did not catch exceptions thrown by the function, so the standard filter, collect and transform combinators on such a future threw synchronously at the call site instead of returning a failed future:

== Future.successful ==      filter/collect/transform with throwing f -> Failure(boom)
== FastFuture.successful ==  filter/collect/transform with throwing f -> thrown synchronously

Callers cannot tell a FastFuture-produced Future from any other, and pekko-grpc now returns them from its public APIs (apache/pekko-grpc#686, apache/pekko-grpc#863), so user code combining those futures hit the difference. map was unaffected because Try.map already catches.

Modification

Route the transform overrides of FulfilledFuture and ErrorFuture through a private helper that converts a NonFatal exception thrown by the function into an ErrorFuture, matching the contract of Future.transform. Both classes are private, so no binary shape changes.

Result

Standard combinators on a completed FastFuture return a failed future when their function throws, the same as for Future.successful / Future.failed.

Tests

  • sbt "http-core/Test/testOnly org.apache.pekko.http.scaladsl.util.FastFutureSpec" - the new transform/filter/collect cases fail without the fix (4 failures), 32/32 pass with it
  • sbt "http-core/mimaReportBinaryIssues" - no issues (compared against 1.0.0)
  • scalafmt --mode diff-ref=upstream/main
  • git diff --check

References

None - found while reviewing FastFuture stack and exception semantics for pekko-grpc's increased use of it

… throws

Motivation:
`FastFuture.successful` / `FastFuture.failed` return already completed futures
that override `Future.transform` to apply the function directly. Unlike
`DefaultPromise`, that override did not catch exceptions thrown by the
function, so the standard `filter`, `collect` and `transform` combinators on
such a future threw synchronously at the call site instead of returning a
failed future. Callers cannot tell a FastFuture-produced `Future` from any
other, and pekko-grpc now returns them from its public APIs.

Modification:
Route both `transform` overrides through a helper that converts a `NonFatal`
exception from the function into an `ErrorFuture`, matching the contract of
`Future.transform`.

Result:
Standard combinators on a completed FastFuture return a failed future when
their function throws, the same as for `Future.successful` / `Future.failed`.

Tests:
- sbt "http-core/Test/testOnly org.apache.pekko.http.scaladsl.util.FastFutureSpec"
  - new transform/filter/collect cases fail without the fix, 32/32 pass with it
- sbt "http-core/mimaReportBinaryIssues" - no issues (against 1.0.0)
- scalafmt --mode diff-ref=upstream/main
- git diff --check

References:
None - found while reviewing FastFuture stack and exception semantics for pekko-grpc
@pjfanning
pjfanning merged commit da54f5e into apache:main Sep 17, 2026
6 checks passed
@pjfanning pjfanning added this to the 2.0.0-M2 milestone Sep 17, 2026
@pjfanning
pjfanning deleted the fastfuture-transform-exceptions branch September 17, 2026 15:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants