Error handling in scopes

How errors are handled depends on the type of concurrency scope that is used.

Supervised scope

The “default” and recommended scope is created using supervised. When this scope is used, any fork created using fork or forkUser that fails with an exception, will cause the enclosing scope to end:

import ox.{forkUser, sleep, supervised}
import scala.concurrent.duration.*

supervised {
  forkUser {
    sleep(100.millis)
    throw new RuntimeException("boom!")  
  }
  forkUser {
    // other forks will be interrupted
  }
}
// will re-throw the "boom!' exception

If an unsupervised fork fails (created using forkUnsupervised / forkCancellable), that exception will be thrown when invoking Fork.join.

Supervised scope with application errors

Additionally, supervised scopes can be created with an error mode, which allows ending the scope when a fork returns a value that is an application error. This can be done by using supervisedError and forkError, for example:

import ox.{EitherMode, forkUserError, supervisedError}

supervisedError(EitherMode[Int]) { 
  forkUserError { Left(10) } 
  Right(()) 
}
// returns Left(10)

Even though the body of the scope returns success (a Right), the scope ends with an application error (a Left), which is reported by a user fork. Note that if we used a daemon fork, the scope might have ended before the error was reported.

Only forks created with forkError and forkUserError can report application errors, and they must return a value of the shape as described by the error mode (in the example above, all forkError, forkUserError and the scope body must return an Either[Int, T] for arbitrary Ts).

The behavior of fork and forkUser in supervisedError scopes is unchanged, that is, their return values are not inspected.

Unsupervised scopes

In an unsupervised scope (created using unsupervised), failures of the forks won’t be reported in any way, unless they are explicitly joined. Hence, if there’s no Fork.join, the exception might go unnoticed.