From 67ca8b79c6ad6dcc2d3777d178f1962ec1966be9 Mon Sep 17 00:00:00 2001 From: Som Snytt Date: Tue, 25 Apr 2023 13:16:34 -0700 Subject: [PATCH 1/5] Em dashes are separated by space --- _overviews/core/futures.md | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/_overviews/core/futures.md b/_overviews/core/futures.md index 9704cd57af..172b3ee0b6 100644 --- a/_overviews/core/futures.md +++ b/_overviews/core/futures.md @@ -14,7 +14,8 @@ permalink: /overviews/core/:title.html ## Introduction Futures provide a way to reason about performing many operations -in parallel-- in an efficient and non-blocking way. +in parallel -- in an efficient and non-blocking way. + A [`Future`](https://www.scala-lang.org/api/current/scala/concurrent/Future.html) is a placeholder object for a value that may not yet exist. Generally, the value of the Future is supplied concurrently and can subsequently be used. @@ -283,7 +284,7 @@ Completion can take one of two forms: A `Future` has an important property that it may only be assigned once. Once a `Future` object is given a value or an exception, it becomes -in effect immutable-- it can never be overwritten. +in effect immutable -- it can never be overwritten. The simplest way to create a future object is to invoke the `Future.apply` method which starts an asynchronous computation and returns a @@ -335,8 +336,8 @@ To obtain the list of friends of a user, a request has to be sent over a network, which can take a long time. This is illustrated with the call to the method `getFriends` that returns `List[Friend]`. To better utilize the CPU until the response arrives, we should not -block the rest of the program-- this computation should be scheduled -asynchronously. The `Future.apply` method does exactly that-- it performs +block the rest of the program -- this computation should be scheduled +asynchronously. The `Future.apply` method does exactly that -- it performs the specified computation block concurrently, in this case sending a request to the server and waiting for a response. @@ -396,7 +397,7 @@ We are often interested in the result of the computation, not just its side-effects. In many future implementations, once the client of the future becomes interested -in its result, it has to block its own computation and wait until the future is completed-- +in its result, it has to block its own computation and wait until the future is completed -- only then can it use the value of the future to continue its own computation. Although this is allowed by the Scala `Future` API as we will show later, from a performance point of view a better way to do it is in a completely @@ -428,7 +429,7 @@ value is a `Throwable`. Coming back to our social network example, let's assume we want to fetch a list of our own recent posts and render them to the screen. We do so by calling a method `getRecentPosts` which returns -a `List[String]`-- a list of recent textual posts: +a `List[String]` -- a list of recent textual posts: {% tabs futures-05 class=tabs-scala-version %} {% tab 'Scala 2' for=futures-05 %} @@ -650,7 +651,7 @@ some other currency. We would have to repeat this pattern within the to reason about. Second, the `purchase` future is not in the scope with the rest of -the code-- it can only be acted upon from within the `foreach` +the code -- it can only be acted upon from within the `foreach` callback. This means that other parts of the application do not see the `purchase` future and cannot register another `foreach` callback to it, for example, to sell some other currency. @@ -760,7 +761,7 @@ Here is an example of `flatMap` and `withFilter` usage within for-comprehensions {% endtabs %} The `purchase` future is completed only once both `usdQuote` -and `chfQuote` are completed-- it depends on the values +and `chfQuote` are completed -- it depends on the values of both these futures so its own computation cannot begin earlier. @@ -1086,7 +1087,7 @@ Here is an example of how to block on the result of a future: In the case that the future fails, the caller is forwarded the exception that the future is failed with. This includes the `failed` -projection-- blocking on it results in a `NoSuchElementException` +projection -- blocking on it results in a `NoSuchElementException` being thrown if the original future is completed successfully. Alternatively, calling `Await.ready` waits until the future becomes @@ -1095,7 +1096,7 @@ that method will not throw an exception if the future is failed. The `Future` trait implements the `Awaitable` trait with methods `ready()` and `result()`. These methods cannot be called directly -by the clients-- they can only be called by the execution context. +by the clients -- they can only be called by the execution context. @@ -1226,7 +1227,7 @@ continues its computation, and finally completes the future `f` with a valid result, by completing promise `p`. Promises can also be completed with a `complete` method which takes -a potential value `Try[T]`-- either a failed result of type `Failure[Throwable]` or a +a potential value `Try[T]` -- either a failed result of type `Failure[Throwable]` or a successful result of type `Success[T]`. Analogous to `success`, calling `failure` and `complete` on a promise that has already From c9b5a90226441ba46a4b269020cae88c90cf645b Mon Sep 17 00:00:00 2001 From: Som Snytt Date: Wed, 26 Apr 2023 10:38:00 -0700 Subject: [PATCH 2/5] Add example for error handling --- _overviews/core/futures.md | 85 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 81 insertions(+), 4 deletions(-) diff --git a/_overviews/core/futures.md b/_overviews/core/futures.md index 172b3ee0b6..51f73cd02b 100644 --- a/_overviews/core/futures.md +++ b/_overviews/core/futures.md @@ -1106,8 +1106,8 @@ When asynchronous computations throw unhandled exceptions, futures associated with those computations fail. Failed futures store an instance of `Throwable` instead of the result value. `Future`s provide the `failed` projection method, which allows this `Throwable` to be -treated as the success value of another `Future`. The following special -exceptions are treated differently: +treated as the success value of another `Future`. +The following exceptions receive special treatment: 1. `scala.runtime.NonLocalReturnControl[_]` -- this exception holds a value associated with the return. Typically, `return` constructs in method @@ -1122,11 +1122,88 @@ behind this is to prevent propagation of critical and control-flow related exceptions normally not handled by the client code and at the same time inform the client in which future the computation failed. -Fatal exceptions (as determined by `NonFatal`) are rethrown in the thread executing +Fatal exceptions (as determined by `NonFatal`) are rethrown from the thread executing the failed asynchronous computation. This informs the code managing the executing threads of the problem and allows it to fail fast, if necessary. See [`NonFatal`](https://www.scala-lang.org/api/current/scala/util/control/NonFatal$.html) -for a more precise description of the semantics. +for a more precise description of which exceptions are considered fatal. + +`ExecutionContext.global` handles fatal exceptions by printing a stack trace, by default. + +A fatal exception means that the `Future` associated with the computation will never complete. +That is, "fatal" means that the error is not recoverable for the `ExecutionContext` +and is also not intended to be handled by user code. By contrast, application code may +attempt recovery from a "failed" `Future`, which has completed but with an exception. + +An execution context can be customized with a reporter that handles fatal exceptions. +See the factory methods [`fromExecutor`](https://www.scala-lang.org/api/current/scala/concurrent/ExecutionContext$.html#fromExecutor(e:java.util.concurrent.Executor,reporter:Throwable=%3EUnit):scala.concurrent.ExecutionContextExecutor) +and [`fromExecutorService`](https://www.scala-lang.org/api/current/scala/concurrent/ExecutionContext$.html#fromExecutorService(e:java.util.concurrent.ExecutorService,reporter:Throwable=%3EUnit):scala.concurrent.ExecutionContextExecutorService). + +Since it is necessary to set the [`UncaughtExceptionHandler`](https://docs.oracle.com/en/java/javase/20/docs/api/java.base/java/lang/Thread.UncaughtExceptionHandler.html) +for executing threads, as a convenience, `fromExecutor` will create a correctly configured context when passed a `null` executor. + +The following example demonstrates how to obtain an `ExecutionContext` with custom error handling +and also shows the result of different exceptions, as described above: + +{% tabs exceptions %} +{% tab 'Scala 2 and 3' for=exceptions %} + import scala.concurrent._, duration._ + import scala.util.chaining._ + import java.util.concurrent.ForkJoinPool + import java.util.concurrent.TimeUnit.SECONDS + + class C { + def crashing(x: Any): Int = throw new NoSuchMethodError("test") + def failing(x: Any): Int = throw new NumberFormatException("test") + def interrupt(x: Any): Int = throw new InterruptedException("test") + def erroring(x: Any): Int = throw new AssertionError("test") + // warning: return statement uses an exception to pass control to the caller of the enclosing named method nonlocally + def nonlocally(xs: List[Int]): Int = { xs.foreach(x => if (x > 0) return x); -1 } + def testCrashes()(implicit ec: ExecutionContext): Future[Int] = Future.unit.map(crashing) + def testFails()(implicit ec: ExecutionContext): Future[Int] = Future.unit.map(failing) + def testInterrupted()(implicit ec: ExecutionContext): Future[Int] = Future.unit.map(interrupt) + def testError()(implicit ec: ExecutionContext): Future[Int] = Future.unit.map(erroring) + def testNonLocal()(implicit ec: ExecutionContext): Future[Int] = Future(List(42)).map(nonlocally) + } + + object Test extends App { + val c = new C + def check(f: => Future[Int]): Future[Int] = + try Await.ready(f, Duration(1L, SECONDS)).tap(res => println(s"ready $res")) + catch { case e: Exception => println(s"threw $e"); Future.never } + def reporter(t: Throwable) = println(s"reported $t") + locally { + import ExecutionContext.Implicits._ + check(c.testFails()) // a failed Future + check(c.testCrashes()) // threw java.util.concurrent.TimeoutException + check(c.testInterrupted()) // a failed Future + .failed + .foreach(e => println(s"failed ${e.getCause}")) // failed java.lang.InterruptedException: test + check(c.testError()) // a failed Future (like InterruptedException) + check(c.testNonLocal()) // a successful Future + } + locally { + implicit val ec: ExecutionContext = ExecutionContext.fromExecutor(null, reporter) + check(c.testCrashes()) // reported java.lang.NoSuchMethodError: test + } + locally { + implicit val ec: ExecutionContext = ExecutionContext.fromExecutor(ForkJoinPool.commonPool(), reporter) + check(c.testCrashes()) // not reported + } + locally { + val ueh: Thread.UncaughtExceptionHandler = (t: Thread, e: Throwable) => reporter(e) + val pool = new ForkJoinPool( + Runtime.getRuntime.availableProcessors, + ForkJoinPool.defaultForkJoinWorkerThreadFactory, + ueh, + /*asyncMode=*/ false + ) + implicit val ec: ExecutionContext = ExecutionContext.fromExecutor(pool, reporter) + check(c.testCrashes()) // reported java.lang.NoSuchMethodError: test + } + } +{% endtab %} +{% endtabs %} ## Promises From 3cc42b97eee88d41a533e603a59f438a6e6ae887 Mon Sep 17 00:00:00 2001 From: Som Snytt Date: Thu, 27 Apr 2023 02:16:13 -0700 Subject: [PATCH 3/5] Simplify and clean-up per review --- _overviews/core/futures.md | 72 +++++++++++++++++++++++--------------- 1 file changed, 43 insertions(+), 29 deletions(-) diff --git a/_overviews/core/futures.md b/_overviews/core/futures.md index 51f73cd02b..809d27c7e3 100644 --- a/_overviews/core/futures.md +++ b/_overviews/core/futures.md @@ -1140,7 +1140,9 @@ See the factory methods [`fromExecutor`](https://www.scala-lang.org/api/current/ and [`fromExecutorService`](https://www.scala-lang.org/api/current/scala/concurrent/ExecutionContext$.html#fromExecutorService(e:java.util.concurrent.ExecutorService,reporter:Throwable=%3EUnit):scala.concurrent.ExecutionContextExecutorService). Since it is necessary to set the [`UncaughtExceptionHandler`](https://docs.oracle.com/en/java/javase/20/docs/api/java.base/java/lang/Thread.UncaughtExceptionHandler.html) -for executing threads, as a convenience, `fromExecutor` will create a correctly configured context when passed a `null` executor. +for executing threads, as a convenience, when passed a `null` executor, +`fromExecutor` will create a context that is configured the same as `global`, +but with the supplied reporter for handling exceptions. The following example demonstrates how to obtain an `ExecutionContext` with custom error handling and also shows the result of different exceptions, as described above: @@ -1152,54 +1154,66 @@ and also shows the result of different exceptions, as described above: import java.util.concurrent.ForkJoinPool import java.util.concurrent.TimeUnit.SECONDS - class C { - def crashing(x: Any): Int = throw new NoSuchMethodError("test") - def failing(x: Any): Int = throw new NumberFormatException("test") - def interrupt(x: Any): Int = throw new InterruptedException("test") - def erroring(x: Any): Int = throw new AssertionError("test") - // warning: return statement uses an exception to pass control to the caller of the enclosing named method nonlocally - def nonlocally(xs: List[Int]): Int = { xs.foreach(x => if (x > 0) return x); -1 } - def testCrashes()(implicit ec: ExecutionContext): Future[Int] = Future.unit.map(crashing) - def testFails()(implicit ec: ExecutionContext): Future[Int] = Future.unit.map(failing) - def testInterrupted()(implicit ec: ExecutionContext): Future[Int] = Future.unit.map(interrupt) - def testError()(implicit ec: ExecutionContext): Future[Int] = Future.unit.map(erroring) - def testNonLocal()(implicit ec: ExecutionContext): Future[Int] = Future(List(42)).map(nonlocally) - } - object Test extends App { - val c = new C - def check(f: => Future[Int]): Future[Int] = + def crashing(): Int = throw new NoSuchMethodError("test") + def failing(): Int = throw new NumberFormatException("test") + def interrupt(): Int = throw new InterruptedException("test") + def erroring(): Int = throw new AssertionError("test") + + // computations can fail in the middle of a chain of combinators, after the initial Future job has completed + def testCrashes()(implicit ec: ExecutionContext): Future[Int] = Future.unit.map(_ => crashing()) + def testFails()(implicit ec: ExecutionContext): Future[Int] = Future.unit.map(_ => failing()) + def testInterrupted()(implicit ec: ExecutionContext): Future[Int] = Future.unit.map(_ => interrupt()) + def testError()(implicit ec: ExecutionContext): Future[Int] = Future.unit.map(_ => erroring()) + + def check(f: Future[Int]): Future[Int] = try Await.ready(f, Duration(1L, SECONDS)).tap(res => println(s"ready $res")) - catch { case e: Exception => println(s"threw $e"); Future.never } + catch { case e: Exception => Future.never.tap(_ => println(s"threw $e")) } + def reporter(t: Throwable) = println(s"reported $t") + locally { + // using the `global` implicit context import ExecutionContext.Implicits._ - check(c.testFails()) // a failed Future - check(c.testCrashes()) // threw java.util.concurrent.TimeoutException - check(c.testInterrupted()) // a failed Future + // a successful Future and a failure, with the output printed by `check` + check(Future(42)) // ready Future(Success(42)) + check(Future(failing())) // ready Future(Failure(java.lang.NumberFormatException: test)) + + // the Future completes with an application exception + check(testFails()) // ready Future(Failure(java.lang.NumberFormatException: test)) + // the Future does not complete because of a linkage error; the trace is printed to stderr by default + check(testCrashes()) // threw java.util.concurrent.TimeoutException: Future timed out after [1 second] + // the Future completes with an operational exception that is wrapped + check(testInterrupted()) // ready Future(Failure(java.util.concurrent.ExecutionException: Boxed Exception)) .failed .foreach(e => println(s"failed ${e.getCause}")) // failed java.lang.InterruptedException: test - check(c.testError()) // a failed Future (like InterruptedException) - check(c.testNonLocal()) // a successful Future + // the Future completes due to a failed assert, which is bad for the app, but is handled the same as interruption + check(testError()) // ready Future(Failure(java.util.concurrent.ExecutionException: Boxed Exception)) + .failed + .foreach(e => println(s"failed ${e.getCause}")) // failed java.lang.AssertionError: test } locally { + // same as `global`, but adds a custom reporter that will handle uncaught exceptions and errors reported to the context implicit val ec: ExecutionContext = ExecutionContext.fromExecutor(null, reporter) - check(c.testCrashes()) // reported java.lang.NoSuchMethodError: test + check(testCrashes()) // reported java.lang.NoSuchMethodError: test } locally { + // does not handle uncaught exceptions; the executor would have to be configured separately implicit val ec: ExecutionContext = ExecutionContext.fromExecutor(ForkJoinPool.commonPool(), reporter) - check(c.testCrashes()) // not reported + // the reporter is not invoked and the Future does not complete + check(testCrashes()) // threw java.util.concurrent.TimeoutException: Future timed out after [1 second] } locally { - val ueh: Thread.UncaughtExceptionHandler = (t: Thread, e: Throwable) => reporter(e) + // sample minimal configuration for a context and underlying pool that use the reporter + val handler: Thread.UncaughtExceptionHandler = (t: Thread, e: Throwable) => reporter(e) val pool = new ForkJoinPool( Runtime.getRuntime.availableProcessors, - ForkJoinPool.defaultForkJoinWorkerThreadFactory, - ueh, + ForkJoinPool.defaultForkJoinWorkerThreadFactory, // threads use the pool's handler + handler, /*asyncMode=*/ false ) implicit val ec: ExecutionContext = ExecutionContext.fromExecutor(pool, reporter) - check(c.testCrashes()) // reported java.lang.NoSuchMethodError: test + check(testCrashes()) // reported java.lang.NoSuchMethodError: test } } {% endtab %} From 3fad134dbe1be2facf4ec8e71d8d51447c44b059 Mon Sep 17 00:00:00 2001 From: Julien Richard-Foy Date: Mon, 1 May 2023 17:49:09 +0200 Subject: [PATCH 4/5] (hopefully) improve the readability of exception handling with Futures Also add the Scala 3 version of the code example --- _overviews/core/futures.md | 257 +++++++++++++++++++++++++++---------- 1 file changed, 190 insertions(+), 67 deletions(-) diff --git a/_overviews/core/futures.md b/_overviews/core/futures.md index 809d27c7e3..c39da4ad0c 100644 --- a/_overviews/core/futures.md +++ b/_overviews/core/futures.md @@ -1147,75 +1147,198 @@ but with the supplied reporter for handling exceptions. The following example demonstrates how to obtain an `ExecutionContext` with custom error handling and also shows the result of different exceptions, as described above: -{% tabs exceptions %} -{% tab 'Scala 2 and 3' for=exceptions %} - import scala.concurrent._, duration._ - import scala.util.chaining._ - import java.util.concurrent.ForkJoinPool - import java.util.concurrent.TimeUnit.SECONDS - - object Test extends App { - def crashing(): Int = throw new NoSuchMethodError("test") - def failing(): Int = throw new NumberFormatException("test") - def interrupt(): Int = throw new InterruptedException("test") - def erroring(): Int = throw new AssertionError("test") - - // computations can fail in the middle of a chain of combinators, after the initial Future job has completed - def testCrashes()(implicit ec: ExecutionContext): Future[Int] = Future.unit.map(_ => crashing()) - def testFails()(implicit ec: ExecutionContext): Future[Int] = Future.unit.map(_ => failing()) - def testInterrupted()(implicit ec: ExecutionContext): Future[Int] = Future.unit.map(_ => interrupt()) - def testError()(implicit ec: ExecutionContext): Future[Int] = Future.unit.map(_ => erroring()) - - def check(f: Future[Int]): Future[Int] = - try Await.ready(f, Duration(1L, SECONDS)).tap(res => println(s"ready $res")) - catch { case e: Exception => Future.never.tap(_ => println(s"threw $e")) } - - def reporter(t: Throwable) = println(s"reported $t") - - locally { - // using the `global` implicit context - import ExecutionContext.Implicits._ - // a successful Future and a failure, with the output printed by `check` - check(Future(42)) // ready Future(Success(42)) - check(Future(failing())) // ready Future(Failure(java.lang.NumberFormatException: test)) - - // the Future completes with an application exception - check(testFails()) // ready Future(Failure(java.lang.NumberFormatException: test)) - // the Future does not complete because of a linkage error; the trace is printed to stderr by default - check(testCrashes()) // threw java.util.concurrent.TimeoutException: Future timed out after [1 second] - // the Future completes with an operational exception that is wrapped - check(testInterrupted()) // ready Future(Failure(java.util.concurrent.ExecutionException: Boxed Exception)) - .failed - .foreach(e => println(s"failed ${e.getCause}")) // failed java.lang.InterruptedException: test - // the Future completes due to a failed assert, which is bad for the app, but is handled the same as interruption - check(testError()) // ready Future(Failure(java.util.concurrent.ExecutionException: Boxed Exception)) - .failed - .foreach(e => println(s"failed ${e.getCause}")) // failed java.lang.AssertionError: test - } - locally { - // same as `global`, but adds a custom reporter that will handle uncaught exceptions and errors reported to the context - implicit val ec: ExecutionContext = ExecutionContext.fromExecutor(null, reporter) - check(testCrashes()) // reported java.lang.NoSuchMethodError: test - } - locally { - // does not handle uncaught exceptions; the executor would have to be configured separately - implicit val ec: ExecutionContext = ExecutionContext.fromExecutor(ForkJoinPool.commonPool(), reporter) - // the reporter is not invoked and the Future does not complete - check(testCrashes()) // threw java.util.concurrent.TimeoutException: Future timed out after [1 second] - } - locally { - // sample minimal configuration for a context and underlying pool that use the reporter - val handler: Thread.UncaughtExceptionHandler = (t: Thread, e: Throwable) => reporter(e) - val pool = new ForkJoinPool( - Runtime.getRuntime.availableProcessors, - ForkJoinPool.defaultForkJoinWorkerThreadFactory, // threads use the pool's handler - handler, - /*asyncMode=*/ false - ) - implicit val ec: ExecutionContext = ExecutionContext.fromExecutor(pool, reporter) - check(testCrashes()) // reported java.lang.NoSuchMethodError: test +{% tabs exceptions class=tabs-scala-version %} +{% tab 'Scala 2' for=exceptions %} +~~~ scala +import scala.concurrent.{Await, ExecutionContext, Future} +import scala.concurrent.duration.DurationInt +import java.util.concurrent.{ForkJoinPool, TimeoutException} +import scala.util.{Failure, Success} + +object Test extends App { + def crashing(): Int = throw new NoSuchMethodError("test") + def failing(): Int = throw new NumberFormatException("test") + def interrupt(): Int = throw new InterruptedException("test") + def erroring(): Int = throw new AssertionError("test") + + // computations can fail in the middle of a chain of combinators, after the initial Future job has completed + def testCrashes()(implicit ec: ExecutionContext): Future[Int] = + Future.unit.map(_ => crashing()) + def testFails()(implicit ec: ExecutionContext): Future[Int] = + Future.unit.map(_ => failing()) + def testInterrupted()(implicit ec: ExecutionContext): Future[Int] = + Future.unit.map(_ => interrupt()) + def testError()(implicit ec: ExecutionContext): Future[Int] = + Future.unit.map(_ => erroring()) + + // Wait for 1 second for the the completion of the passed `future` value and print it + def check(future: Future[Int]): Unit = + try { + Await.ready(future, 1.second) + for (completion <- future.value) { + println(s"completed $completion") + // In case of failure, also print the cause of the exception, when defined + completion match { + case Failure(exception) if exception.getCause != null => + println(s" caused by ${exception.getCause}") + _ => () + } } + } catch { + // If the future value did not complete within 1 second, the call + // to `Await.ready` throws a TimeoutException + case _: TimeoutException => println(s"did not complete") } + + def reporter(t: Throwable) = println(s"reported $t") + + locally { + // using the `global` implicit context + import ExecutionContext.Implicits._ + // a successful Future + check(Future(42)) // completed Success(42) + // a Future that completes with an application exception + check(Future(failing())) // completed Failure(java.lang.NumberFormatException: test) + // same, but the exception is thrown somewhere in the chain of combinators + check(testFails()) // completed Failure(java.lang.NumberFormatException: test) + // a Future that does not complete because of a linkage error; + // the trace is printed to stderr by default + check(testCrashes()) // did not complete + // a Future that completes with an operational exception that is wrapped + check(testInterrupted()) // completed Failure(java.util.concurrent.ExecutionException: Boxed Exception) + // caused by java.lang.InterruptedException: test + // a Future that completes due to a failed assert, which is bad for the app, + // but is handled the same as interruption + check(testError()) // completed Failure(java.util.concurrent.ExecutionException: Boxed Exception) + // caused by java.lang.AssertionError: test + } + locally { + // same as `global`, but adds a custom reporter that will handle uncaught + // exceptions and errors reported to the context + implicit val ec: ExecutionContext = ExecutionContext.fromExecutor(null, reporter) + check(testCrashes()) // reported java.lang.NoSuchMethodError: test + // did not complete + } + locally { + // does not handle uncaught exceptions; the executor would have to be + // configured separately + val executor = ForkJoinPool.commonPool() + implicit val ec: ExecutionContext = ExecutionContext.fromExecutor(executor, reporter) + // the reporter is not invoked and the Future does not complete + check(testCrashes()) // did not complete + } + locally { + // sample minimal configuration for a context and underlying pool that + // use the reporter + val handler: Thread.UncaughtExceptionHandler = + (_: Thread, t: Throwable) => reporter(t) + val executor = new ForkJoinPool( + Runtime.getRuntime.availableProcessors, + ForkJoinPool.defaultForkJoinWorkerThreadFactory, // threads use the pool's handler + handler, + /*asyncMode=*/ false + ) + implicit val ec: ExecutionContext = ExecutionContext.fromExecutor(executor, reporter) + check(testCrashes()) // reported java.lang.NoSuchMethodError: test + // did not complete + } +} +~~~ +{% endtab %} + +{% tab 'Scala 3' for=exceptions %} +~~~ scala +import scala.concurrent.{Await, ExecutionContext, Future} +import scala.concurrent.duration.DurationInt +import java.util.concurrent.{ForkJoinPool, TimeoutException} +import scala.util.{Failure, Success} + +def crashing(): Int = throw new NoSuchMethodError("test") +def failing(): Int = throw new NumberFormatException("test") +def interrupt(): Int = throw new InterruptedException("test") +def erroring(): Int = throw new AssertionError("test") + +// computations can fail in the middle of a chain of combinators, +// after the initial Future job has completed +def testCrashes()(using ExecutionContext): Future[Int] = + Future.unit.map(_ => crashing()) +def testFails()(using ExecutionContext): Future[Int] = + Future.unit.map(_ => failing()) +def testInterrupted()(using ExecutionContext): Future[Int] = + Future.unit.map(_ => interrupt()) +def testError()(using ExecutionContext): Future[Int] = + Future.unit.map(_ => erroring()) + +// Wait for 1 second for the the completion of the passed `future` value and print it +def check(future: Future[Int]): Unit = + try + Await.ready(future, 1.second) + for completion <- future.value do + println(s"completed $completion") + // In case of failure, also print the cause of the exception, when defined + completion match + case Failure(exception) if exception.getCause != null => + println(s" caused by ${exception.getCause}") + case _ => () + catch + // If the future value did not complete within 1 second, the call + // to `Await.ready` throws a TimeoutException + case _: TimeoutException => println(s"did not complete") + +def reporter(t: Throwable) = println(s"reported $t") + +@main def test(): Unit = + locally: + // using the `global` implicit context + import ExecutionContext.Implicits.* + // a successful Future + check(Future(42)) // completed Success(42) + // a Future that completes with an application exception + check(Future(failing())) // completed Failure(java.lang.NumberFormatException: test) + // same, but the exception is thrown somewhere in the chain of combinators + check(testFails()) // completed Failure(java.lang.NumberFormatException: test) + // a Future that does not complete because of a linkage error; + // the trace is printed to stderr by default + check(testCrashes()) // did not complete + // a Future that completes with an operational exception that is wrapped + check(testInterrupted()) // completed Failure(java.util.concurrent.ExecutionException: Boxed Exception) + // caused by java.lang.InterruptedException: test + // a Future that completes due to a failed assert, which is bad for the app, + // but is handled the same as interruption + check(testError()) // completed Failure(java.util.concurrent.ExecutionException: Boxed Exception) + // caused by java.lang.AssertionError: test + + locally: + // same as `global`, but adds a custom reporter that will handle uncaught + // exceptions and errors reported to the context + given ExecutionContext = ExecutionContext.fromExecutor(null, reporter) + check(testCrashes()) // reported java.lang.NoSuchMethodError: test + // did not complete + + locally: + // does not handle uncaught exceptions; the executor would have to be + // configured separately + val executor = ForkJoinPool.commonPool() + given ExecutionContext = ExecutionContext.fromExecutor(executor, reporter) + // the reporter is not invoked and the Future does not complete + check(testCrashes()) // did not complete + + locally: + // sample minimal configuration for a context and underlying pool that + // use the reporter + val handler: Thread.UncaughtExceptionHandler = + (_: Thread, t: Throwable) => reporter(t) + val executor = new ForkJoinPool( + Runtime.getRuntime.availableProcessors, + ForkJoinPool.defaultForkJoinWorkerThreadFactory, // threads use the pool's handler + handler, + /*asyncMode=*/ false + ) + given ExecutionContext = ExecutionContext.fromExecutor(executor, reporter) + check(testCrashes()) // reported java.lang.NoSuchMethodError: test + // did not complete +end test +~~~ {% endtab %} {% endtabs %} From f71f92d8d987470a45b7feb5d3c8a62ae729c59a Mon Sep 17 00:00:00 2001 From: Julien Richard-Foy Date: Tue, 2 May 2023 09:19:53 +0200 Subject: [PATCH 5/5] Get more style points --- _overviews/core/futures.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/_overviews/core/futures.md b/_overviews/core/futures.md index c39da4ad0c..aac3aa4b4f 100644 --- a/_overviews/core/futures.md +++ b/_overviews/core/futures.md @@ -1150,9 +1150,9 @@ and also shows the result of different exceptions, as described above: {% tabs exceptions class=tabs-scala-version %} {% tab 'Scala 2' for=exceptions %} ~~~ scala +import java.util.concurrent.{ForkJoinPool, TimeoutException} import scala.concurrent.{Await, ExecutionContext, Future} import scala.concurrent.duration.DurationInt -import java.util.concurrent.{ForkJoinPool, TimeoutException} import scala.util.{Failure, Success} object Test extends App { @@ -1248,9 +1248,9 @@ object Test extends App { {% tab 'Scala 3' for=exceptions %} ~~~ scala +import java.util.concurrent.{ForkJoinPool, TimeoutException} import scala.concurrent.{Await, ExecutionContext, Future} import scala.concurrent.duration.DurationInt -import java.util.concurrent.{ForkJoinPool, TimeoutException} import scala.util.{Failure, Success} def crashing(): Int = throw new NoSuchMethodError("test") @@ -1290,7 +1290,7 @@ def reporter(t: Throwable) = println(s"reported $t") @main def test(): Unit = locally: // using the `global` implicit context - import ExecutionContext.Implicits.* + import ExecutionContext.Implicits.given // a successful Future check(Future(42)) // completed Success(42) // a Future that completes with an application exception