Skip to content

Commit a2f2239

Browse files
authored
Add example for error handling in ExecutionContext (#2777)
1 parent 0b6ecb9 commit a2f2239

File tree

1 file changed

+230
-15
lines changed

1 file changed

+230
-15
lines changed

_overviews/core/futures.md

Lines changed: 230 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ permalink: /overviews/core/:title.html
1414
## Introduction
1515

1616
Futures provide a way to reason about performing many operations
17-
in parallel-- in an efficient and non-blocking way.
17+
in parallel -- in an efficient and non-blocking way.
18+
1819
A [`Future`](https://www.scala-lang.org/api/current/scala/concurrent/Future.html)
1920
is a placeholder object for a value that may not yet exist.
2021
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:
283284
A `Future` has an important property that it may only be assigned
284285
once.
285286
Once a `Future` object is given a value or an exception, it becomes
286-
in effect immutable-- it can never be overwritten.
287+
in effect immutable -- it can never be overwritten.
287288

288289
The simplest way to create a future object is to invoke the `Future.apply`
289290
method which starts an asynchronous computation and returns a
@@ -335,8 +336,8 @@ To obtain the list of friends of a user, a request
335336
has to be sent over a network, which can take a long time.
336337
This is illustrated with the call to the method `getFriends` that returns `List[Friend]`.
337338
To better utilize the CPU until the response arrives, we should not
338-
block the rest of the program-- this computation should be scheduled
339-
asynchronously. The `Future.apply` method does exactly that-- it performs
339+
block the rest of the program -- this computation should be scheduled
340+
asynchronously. The `Future.apply` method does exactly that -- it performs
340341
the specified computation block concurrently, in this case sending
341342
a request to the server and waiting for a response.
342343

@@ -396,7 +397,7 @@ We are often interested in the result of the computation, not just its
396397
side-effects.
397398

398399
In many future implementations, once the client of the future becomes interested
399-
in its result, it has to block its own computation and wait until the future is completed--
400+
in its result, it has to block its own computation and wait until the future is completed --
400401
only then can it use the value of the future to continue its own computation.
401402
Although this is allowed by the Scala `Future` API as we will show later,
402403
from a performance point of view a better way to do it is in a completely
@@ -428,7 +429,7 @@ value is a `Throwable`.
428429
Coming back to our social network example, let's assume we want to
429430
fetch a list of our own recent posts and render them to the screen.
430431
We do so by calling a method `getRecentPosts` which returns
431-
a `List[String]`-- a list of recent textual posts:
432+
a `List[String]` -- a list of recent textual posts:
432433

433434
{% tabs futures-05 class=tabs-scala-version %}
434435
{% tab 'Scala 2' for=futures-05 %}
@@ -650,7 +651,7 @@ some other currency. We would have to repeat this pattern within the
650651
to reason about.
651652

652653
Second, the `purchase` future is not in the scope with the rest of
653-
the code-- it can only be acted upon from within the `foreach`
654+
the code -- it can only be acted upon from within the `foreach`
654655
callback. This means that other parts of the application do not
655656
see the `purchase` future and cannot register another `foreach`
656657
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
760761
{% endtabs %}
761762

762763
The `purchase` future is completed only once both `usdQuote`
763-
and `chfQuote` are completed-- it depends on the values
764+
and `chfQuote` are completed -- it depends on the values
764765
of both these futures so its own computation cannot begin
765766
earlier.
766767

@@ -1086,7 +1087,7 @@ Here is an example of how to block on the result of a future:
10861087

10871088
In the case that the future fails, the caller is forwarded the
10881089
exception that the future is failed with. This includes the `failed`
1089-
projection-- blocking on it results in a `NoSuchElementException`
1090+
projection -- blocking on it results in a `NoSuchElementException`
10901091
being thrown if the original future is completed successfully.
10911092

10921093
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.
10951096

10961097
The `Future` trait implements the `Awaitable` trait with methods
10971098
`ready()` and `result()`. These methods cannot be called directly
1098-
by the clients-- they can only be called by the execution context.
1099+
by the clients -- they can only be called by the execution context.
10991100

11001101

11011102

@@ -1105,8 +1106,8 @@ When asynchronous computations throw unhandled exceptions, futures
11051106
associated with those computations fail. Failed futures store an
11061107
instance of `Throwable` instead of the result value. `Future`s provide
11071108
the `failed` projection method, which allows this `Throwable` to be
1108-
treated as the success value of another `Future`. The following special
1109-
exceptions are treated differently:
1109+
treated as the success value of another `Future`.
1110+
The following exceptions receive special treatment:
11101111

11111112
1. `scala.runtime.NonLocalReturnControl[_]` -- this exception holds a value
11121113
associated with the return. Typically, `return` constructs in method
@@ -1121,11 +1122,225 @@ behind this is to prevent propagation of critical and control-flow related
11211122
exceptions normally not handled by the client code and at the same time inform
11221123
the client in which future the computation failed.
11231124

1124-
Fatal exceptions (as determined by `NonFatal`) are rethrown in the thread executing
1125+
Fatal exceptions (as determined by `NonFatal`) are rethrown from the thread executing
11251126
the failed asynchronous computation. This informs the code managing the executing
11261127
threads of the problem and allows it to fail fast, if necessary. See
11271128
[`NonFatal`](https://www.scala-lang.org/api/current/scala/util/control/NonFatal$.html)
1128-
for a more precise description of the semantics.
1129+
for a more precise description of which exceptions are considered fatal.
1130+
1131+
`ExecutionContext.global` handles fatal exceptions by printing a stack trace, by default.
1132+
1133+
A fatal exception means that the `Future` associated with the computation will never complete.
1134+
That is, "fatal" means that the error is not recoverable for the `ExecutionContext`
1135+
and is also not intended to be handled by user code. By contrast, application code may
1136+
attempt recovery from a "failed" `Future`, which has completed but with an exception.
1137+
1138+
An execution context can be customized with a reporter that handles fatal exceptions.
1139+
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)
1140+
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).
1141+
1142+
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)
1143+
for executing threads, as a convenience, when passed a `null` executor,
1144+
`fromExecutor` will create a context that is configured the same as `global`,
1145+
but with the supplied reporter for handling exceptions.
1146+
1147+
The following example demonstrates how to obtain an `ExecutionContext` with custom error handling
1148+
and also shows the result of different exceptions, as described above:
1149+
1150+
{% tabs exceptions class=tabs-scala-version %}
1151+
{% tab 'Scala 2' for=exceptions %}
1152+
~~~ scala
1153+
import java.util.concurrent.{ForkJoinPool, TimeoutException}
1154+
import scala.concurrent.{Await, ExecutionContext, Future}
1155+
import scala.concurrent.duration.DurationInt
1156+
import scala.util.{Failure, Success}
1157+
1158+
object Test extends App {
1159+
def crashing(): Int = throw new NoSuchMethodError("test")
1160+
def failing(): Int = throw new NumberFormatException("test")
1161+
def interrupt(): Int = throw new InterruptedException("test")
1162+
def erroring(): Int = throw new AssertionError("test")
1163+
1164+
// computations can fail in the middle of a chain of combinators, after the initial Future job has completed
1165+
def testCrashes()(implicit ec: ExecutionContext): Future[Int] =
1166+
Future.unit.map(_ => crashing())
1167+
def testFails()(implicit ec: ExecutionContext): Future[Int] =
1168+
Future.unit.map(_ => failing())
1169+
def testInterrupted()(implicit ec: ExecutionContext): Future[Int] =
1170+
Future.unit.map(_ => interrupt())
1171+
def testError()(implicit ec: ExecutionContext): Future[Int] =
1172+
Future.unit.map(_ => erroring())
1173+
1174+
// Wait for 1 second for the the completion of the passed `future` value and print it
1175+
def check(future: Future[Int]): Unit =
1176+
try {
1177+
Await.ready(future, 1.second)
1178+
for (completion <- future.value) {
1179+
println(s"completed $completion")
1180+
// In case of failure, also print the cause of the exception, when defined
1181+
completion match {
1182+
case Failure(exception) if exception.getCause != null =>
1183+
println(s" caused by ${exception.getCause}")
1184+
_ => ()
1185+
}
1186+
}
1187+
} catch {
1188+
// If the future value did not complete within 1 second, the call
1189+
// to `Await.ready` throws a TimeoutException
1190+
case _: TimeoutException => println(s"did not complete")
1191+
}
1192+
1193+
def reporter(t: Throwable) = println(s"reported $t")
1194+
1195+
locally {
1196+
// using the `global` implicit context
1197+
import ExecutionContext.Implicits._
1198+
// a successful Future
1199+
check(Future(42)) // completed Success(42)
1200+
// a Future that completes with an application exception
1201+
check(Future(failing())) // completed Failure(java.lang.NumberFormatException: test)
1202+
// same, but the exception is thrown somewhere in the chain of combinators
1203+
check(testFails()) // completed Failure(java.lang.NumberFormatException: test)
1204+
// a Future that does not complete because of a linkage error;
1205+
// the trace is printed to stderr by default
1206+
check(testCrashes()) // did not complete
1207+
// a Future that completes with an operational exception that is wrapped
1208+
check(testInterrupted()) // completed Failure(java.util.concurrent.ExecutionException: Boxed Exception)
1209+
// caused by java.lang.InterruptedException: test
1210+
// a Future that completes due to a failed assert, which is bad for the app,
1211+
// but is handled the same as interruption
1212+
check(testError()) // completed Failure(java.util.concurrent.ExecutionException: Boxed Exception)
1213+
// caused by java.lang.AssertionError: test
1214+
}
1215+
locally {
1216+
// same as `global`, but adds a custom reporter that will handle uncaught
1217+
// exceptions and errors reported to the context
1218+
implicit val ec: ExecutionContext = ExecutionContext.fromExecutor(null, reporter)
1219+
check(testCrashes()) // reported java.lang.NoSuchMethodError: test
1220+
// did not complete
1221+
}
1222+
locally {
1223+
// does not handle uncaught exceptions; the executor would have to be
1224+
// configured separately
1225+
val executor = ForkJoinPool.commonPool()
1226+
implicit val ec: ExecutionContext = ExecutionContext.fromExecutor(executor, reporter)
1227+
// the reporter is not invoked and the Future does not complete
1228+
check(testCrashes()) // did not complete
1229+
}
1230+
locally {
1231+
// sample minimal configuration for a context and underlying pool that
1232+
// use the reporter
1233+
val handler: Thread.UncaughtExceptionHandler =
1234+
(_: Thread, t: Throwable) => reporter(t)
1235+
val executor = new ForkJoinPool(
1236+
Runtime.getRuntime.availableProcessors,
1237+
ForkJoinPool.defaultForkJoinWorkerThreadFactory, // threads use the pool's handler
1238+
handler,
1239+
/*asyncMode=*/ false
1240+
)
1241+
implicit val ec: ExecutionContext = ExecutionContext.fromExecutor(executor, reporter)
1242+
check(testCrashes()) // reported java.lang.NoSuchMethodError: test
1243+
// did not complete
1244+
}
1245+
}
1246+
~~~
1247+
{% endtab %}
1248+
1249+
{% tab 'Scala 3' for=exceptions %}
1250+
~~~ scala
1251+
import java.util.concurrent.{ForkJoinPool, TimeoutException}
1252+
import scala.concurrent.{Await, ExecutionContext, Future}
1253+
import scala.concurrent.duration.DurationInt
1254+
import scala.util.{Failure, Success}
1255+
1256+
def crashing(): Int = throw new NoSuchMethodError("test")
1257+
def failing(): Int = throw new NumberFormatException("test")
1258+
def interrupt(): Int = throw new InterruptedException("test")
1259+
def erroring(): Int = throw new AssertionError("test")
1260+
1261+
// computations can fail in the middle of a chain of combinators,
1262+
// after the initial Future job has completed
1263+
def testCrashes()(using ExecutionContext): Future[Int] =
1264+
Future.unit.map(_ => crashing())
1265+
def testFails()(using ExecutionContext): Future[Int] =
1266+
Future.unit.map(_ => failing())
1267+
def testInterrupted()(using ExecutionContext): Future[Int] =
1268+
Future.unit.map(_ => interrupt())
1269+
def testError()(using ExecutionContext): Future[Int] =
1270+
Future.unit.map(_ => erroring())
1271+
1272+
// Wait for 1 second for the the completion of the passed `future` value and print it
1273+
def check(future: Future[Int]): Unit =
1274+
try
1275+
Await.ready(future, 1.second)
1276+
for completion <- future.value do
1277+
println(s"completed $completion")
1278+
// In case of failure, also print the cause of the exception, when defined
1279+
completion match
1280+
case Failure(exception) if exception.getCause != null =>
1281+
println(s" caused by ${exception.getCause}")
1282+
case _ => ()
1283+
catch
1284+
// If the future value did not complete within 1 second, the call
1285+
// to `Await.ready` throws a TimeoutException
1286+
case _: TimeoutException => println(s"did not complete")
1287+
1288+
def reporter(t: Throwable) = println(s"reported $t")
1289+
1290+
@main def test(): Unit =
1291+
locally:
1292+
// using the `global` implicit context
1293+
import ExecutionContext.Implicits.given
1294+
// a successful Future
1295+
check(Future(42)) // completed Success(42)
1296+
// a Future that completes with an application exception
1297+
check(Future(failing())) // completed Failure(java.lang.NumberFormatException: test)
1298+
// same, but the exception is thrown somewhere in the chain of combinators
1299+
check(testFails()) // completed Failure(java.lang.NumberFormatException: test)
1300+
// a Future that does not complete because of a linkage error;
1301+
// the trace is printed to stderr by default
1302+
check(testCrashes()) // did not complete
1303+
// a Future that completes with an operational exception that is wrapped
1304+
check(testInterrupted()) // completed Failure(java.util.concurrent.ExecutionException: Boxed Exception)
1305+
// caused by java.lang.InterruptedException: test
1306+
// a Future that completes due to a failed assert, which is bad for the app,
1307+
// but is handled the same as interruption
1308+
check(testError()) // completed Failure(java.util.concurrent.ExecutionException: Boxed Exception)
1309+
// caused by java.lang.AssertionError: test
1310+
1311+
locally:
1312+
// same as `global`, but adds a custom reporter that will handle uncaught
1313+
// exceptions and errors reported to the context
1314+
given ExecutionContext = ExecutionContext.fromExecutor(null, reporter)
1315+
check(testCrashes()) // reported java.lang.NoSuchMethodError: test
1316+
// did not complete
1317+
1318+
locally:
1319+
// does not handle uncaught exceptions; the executor would have to be
1320+
// configured separately
1321+
val executor = ForkJoinPool.commonPool()
1322+
given ExecutionContext = ExecutionContext.fromExecutor(executor, reporter)
1323+
// the reporter is not invoked and the Future does not complete
1324+
check(testCrashes()) // did not complete
1325+
1326+
locally:
1327+
// sample minimal configuration for a context and underlying pool that
1328+
// use the reporter
1329+
val handler: Thread.UncaughtExceptionHandler =
1330+
(_: Thread, t: Throwable) => reporter(t)
1331+
val executor = new ForkJoinPool(
1332+
Runtime.getRuntime.availableProcessors,
1333+
ForkJoinPool.defaultForkJoinWorkerThreadFactory, // threads use the pool's handler
1334+
handler,
1335+
/*asyncMode=*/ false
1336+
)
1337+
given ExecutionContext = ExecutionContext.fromExecutor(executor, reporter)
1338+
check(testCrashes()) // reported java.lang.NoSuchMethodError: test
1339+
// did not complete
1340+
end test
1341+
~~~
1342+
{% endtab %}
1343+
{% endtabs %}
11291344

11301345
## Promises
11311346

@@ -1226,7 +1441,7 @@ continues its computation, and finally completes the future `f` with a
12261441
valid result, by completing promise `p`.
12271442

12281443
Promises can also be completed with a `complete` method which takes
1229-
a potential value `Try[T]`-- either a failed result of type `Failure[Throwable]` or a
1444+
a potential value `Try[T]` -- either a failed result of type `Failure[Throwable]` or a
12301445
successful result of type `Success[T]`.
12311446

12321447
Analogous to `success`, calling `failure` and `complete` on a promise that has already

0 commit comments

Comments
 (0)