Skip to content

#4505 Not Propagating Fatal Errors - #4678

Draft
Frodo2412 wants to merge 4 commits into
typelevel:series/3.xfrom
Frodo2412:fix/4505-out-of-memory-not-propagated
Draft

Frodo2412 wants to merge 4 commits into
typelevel:series/3.xfrom
Frodo2412:fix/4505-out-of-memory-not-propagated

Conversation

@Frodo2412

@Frodo2412 Frodo2412 commented Sep 2, 2026

Copy link
Copy Markdown

Fixes #4505.

  • Adds new test, using example AsyncFatalError
  • Adds fatal error checking within IOFiber.runLoop run loop for case IOCont, before continuing run loop
  • Adds fatal error checking within IOFiber.asyncContinueFailedR for asynchronous continuation

@Frodo2412

Copy link
Copy Markdown
Author

Made test out of this example, but it seems like its green. Could there be something wrong with how I made the test?

object AsyncFatalError extends IOApp {
def run(args: List[String]): IO[ExitCode] = {
IO.async_[Unit](cb => cb(Left(new OutOfMemoryError("Boom!"))))
.flatMap(_ => IO.println("sadness"))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The original reproducer in #4505 runs the async on a fiber and joins it:

        _ <- pingFiber.join
      } yield ExitCode.Success

join returns an object with the outcome of the fiber. This ignores the actual outcome of the async call. Your implementation doesn't use a fiber, so the failure is still propagated and causes your IO to fail. pingFiber also fails, the issue is it doesn't fatally fail.

The core issue with #4505 is that the code shouldn't even get far enough that there is a join result, as the OOM should kill the app immediately. You can probably reproduce this with just an attempt instead of a start/join

Suggested change
.flatMap(_ => IO.println("sadness"))
.attempt
.flatMap(_ => IO.println("sadness"))

or to more closely replicate the original, something like:

Suggested change
.flatMap(_ => IO.println("sadness"))
.start
.flatMap(_.join)
.flatMap(_ => IO.println("sadness"))

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied the second change here, it made the test red: ec8421b

Thanks!

@Frodo2412
Frodo2412 force-pushed the fix/4505-out-of-memory-not-propagated branch from 448a5dd to 31ea3e2 Compare September 12, 2026 20:42
@Frodo2412

Copy link
Copy Markdown
Author

@reardonj I made the test red and added turned them green. Added separate test for the completable future case and it seems to be working as well. Does this look like the right approach?

@reardonj reardonj left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This solution makes sense to me, though I'm not especially familiar with Cont implementation details. @durban did note that there are some edge cases:

A tricky thing with cont is that the callback might be called with something which might not be used. (Multiple results, and also cont can complete synchronously.)

I think one of these would be eg. the fiber is canceled then the callback is invoked. I don't know how we would handle that offhand since the fiber might already be complete. Hopefully a maintainer can help a bit there. This looks like it covers the normal flows though!

CompletableFuture.runAsync(
() => {
println("Waiting 2 seconds before boom...")
Thread.sleep(2000)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason to sleep?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just copied the example verbatim from the original ticket, the wait is there: #4505

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.

OutOfMemoryError not propagated when IO originates from CompletableFuture

2 participants