Skip to main content
Version: 2.x

Sandboxing

We know that a ZIO effect may fail due to a failure, a defect, a fiber interruption, or a combination of these causes. So a ZIO effect may contain more than one cause. Using the ZIO#sandbox operator, we can sandbox all errors of a ZIO application, whether the cause is a failure, defect, or a fiber interruption or combination of these. This operator exposes the full cause of a ZIO effect into the error channel:

trait ZIO[-R, +E, +A] {
def sandbox: ZIO[R, Cause[E], A]
}

When to Use​

Use ZIO#sandbox when you need to apply operators other than catchAllCause or catchSomeCause to the full Cause graph — for example, using ZIO#catchSome or ZIO#mapError against defects and interruptions as well as typed failures. Use ZIO#catchAllCause or ZIO#catchSomeCause directly instead when a single pattern-match-and-recover step is all you need; those operators reach the full Cause without an explicit sandbox/unsandbox round-trip. ZIO#sandboxWith combines the sandbox → operate → unsandbox round-trip in a single call when a more composable form is convenient.

We can use the ZIO#sandbox operator to uncover the full causes of an exceptional effect. So we can see all the errors that occurred as a type of Cause[E] at the error channel of the ZIO data type. So then we can use normal error-handling operators such as ZIO#catchSome and ZIO#catchAll operators:

import zio._

object MainApp extends ZIOAppDefault {
val effect: ZIO[Any, String, String] =
ZIO.succeed("primary result") *> ZIO.fail("Oh uh!")

val myApp: ZIO[Any, Cause[String], String] =
effect.sandbox.catchSome {
case Cause.Interrupt(fiberId, _) =>
ZIO.debug(s"Caught interruption of a fiber with id: $fiberId") *>
ZIO.succeed("fallback result on fiber interruption")
case Cause.Die(value, _) =>
ZIO.debug(s"Caught a defect: $value") *>
ZIO.succeed("fallback result on defect")
case Cause.Fail(value, _) =>
ZIO.debug(s"Caught a failure: $value") *>
ZIO.succeed("fallback result on failure")
}

val finalApp: ZIO[Any, String, String] = myApp.unsandbox.debug("final result")

def run = finalApp
}

// Output:
// Caught a failure: Oh uh!
// final result: fallback result on failure

Using the sandbox operation we are exposing the full cause of an effect. So then we have access to the underlying cause in more detail. After handling exposed causes using ZIO#catch* operators, we can undo the sandbox operation using the unsandbox operation. It will submerge the full cause (Cause[E]) again:

import zio._

val effect: ZIO[Any, String, String] =
ZIO.succeed("primary result") *> ZIO.fail("Oh uh!")

effect // ZIO[Any, String, String]
.sandbox // ZIO[Any, Cause[String], String]
.catchSome(???) // ZIO[Any, Cause[String], String]
.unsandbox // ZIO[Any, String, String]

There is another version of sandbox called ZIO#sandboxWith. This operator helps us to sandbox, then catch all causes, and then unsandbox back:

trait ZIO[-R, +E, +A] {
def sandboxWith[R1 <: R, E2, B](f: ZIO[R1, Cause[E], A] => ZIO[R1, Cause[E2], B]): ZIO[R1, E2, B]
}

Let's try the previous example using this operator:

import zio._

object MainApp extends ZIOAppDefault {
val effect: ZIO[Any, String, String] =
ZIO.succeed("primary result") *> ZIO.fail("Oh uh!")

val myApp =
effect.sandboxWith[Any, String, String] { e =>
e.catchSome {
case Cause.Interrupt(fiberId, _) =>
ZIO.debug(s"Caught interruption of a fiber with id: $fiberId") *>
ZIO.succeed("fallback result on fiber interruption")
case Cause.Die(value, _) =>
ZIO.debug(s"Caught a defect: $value") *>
ZIO.succeed("fallback result on defect")
case Cause.Fail(value, _) =>
ZIO.debug(s"Caught a failure: $value") *>
ZIO.succeed("fallback result on failure")
}
}
def run = myApp.debug
}

// Output:
// Caught a failure: Oh uh!
// fallback result on failure

There is also a companion-object form, ZIO.unsandbox, which accepts the sandboxed effect as a parameter rather than being called as an instance method. Its signature is:

object ZIO {
def unsandbox[R, E, A](v: => ZIO[R, Cause[E], A])(implicit trace: Trace): ZIO[R, E, A]
}

This is useful when passing a sandboxed effect to a helper function or when you want to build a pipeline:

import zio._

def recoverFromCause[R, A](
sandboxedEffect: ZIO[R, Cause[String], A]
): ZIO[R, String, A] =
ZIO.unsandbox(sandboxedEffect)

Both ZIO#unsandbox (the instance method) and ZIO.unsandbox(v) (the companion form) produce the same result — choose whichever reads more naturally in context.