Skip to main content
Version: ZIO 2.x

Runtime

A Runtime[R] is capable of executing tasks within an environment R.

To run an effect, we need a Runtime, which is capable of executing effects. Runtimes bundle a thread pool together with the environment that effects need.

What is a Runtime System?

Whenever we write a ZIO program, we create a ZIO effect from ZIO constructors plus using its combinators. We are building a blueprint. ZIO effect is just a data structure that describes the execution of a concurrent program. So we end up with a tree data structure that contains lots of different data structures combined together to describe what the ZIO effect should do. This data structure doesn't do anything, it is just a description of a concurrent program.

So the most important thing we should keep in mind when we are working with a functional effect system like ZIO is that when we are writing code, printing a string onto the console, reading a file, querying a database, and so forth; We are just writing a workflow or blueprint of an application. We are just building a data structure.

So how can ZIO run these workflows? This is where ZIO Runtime System comes into play. Whenever we run an unsaferun function, the Runtime System is responsible to step through all the instructions described by the ZIO effect and execute them.

To simplify everything, we can think of a Runtime System like a black box that takes both the ZIO effect (ZIO[R, E, A]) and its environment (R), it will run this effect and then will return its result as an Either[E, A] value.

ZIO Runtime System

Responsibilities of the Runtime System

Runtime Systems have a lot of responsibilities:

  1. Execute every step of the blueprint — They have to execute every step of the blueprint in a while loop until it's done.

  2. Handle unexpected errors — They have to handle unexpected errors, not just the expected ones but also the unexpected ones.

  3. Spawn concurrent fiber — They are actually responsible for the concurrency that effect systems have. They have to spawn a fiber every time we call fork on an effect to spawn off a new fiber.

  4. Cooperatively yield to other fibers — They have to cooperatively yield to other fibers so that fibers that are sort of hogging the spotlight, don't get to monopolize all the CPU resources. They have to make sure that the fibers split the CPU cores among all the fibers that are working.

  5. Capture execution and stack traces — They have to keep track of where we are in the progress of our own user-land code so the nice detailed execution traces can be captured.

  6. Ensure finalizers are run appropriately — They have to ensure finalizers are run appropriately at the right point in all circumstances to make sure that resources are closed that clean-up logic is executed. This is the feature that powers Scope and all the other resource-safe constructs in ZIO.

  7. Handle asynchronous callback — They have to handle this messy job of dealing with asynchronous callbacks. So we don't have to deal with async code. When we are doing ZIO, everything is just async out of the box.

Running a ZIO Effect

There are two common ways to run a ZIO effect. Most of the time, we use the ZIOAppDefault trait. There are, however, some advanced use cases for which we need to directly feed a ZIO effect into the runtime system's unsafeRun method:

import zio._

object RunZIOEffectUsingUnsafeRun extends scala.App {
val myAppLogic = for {
_ <- Console.printLine("Hello! What is your name?")
n <- Console.readLine
_ <- Console.printLine("Hello, " + n + ", good to meet you!")
} yield ()

Unsafe.unsafe { implicit unsafe =>
zio.Runtime.default.unsafe.run(
myAppLogic
).getOrThrowFiberFailure()
}
}

We don't usually use this method to run our effects. One of the use cases of this method is when we are integrating the legacy (non-effectful code) with the ZIO effect. It also helps us to refactor a large legacy code base into a ZIO effect gradually; Assume we have decided to refactor a component in the middle of a legacy code and rewrite that with ZIO. We can start rewriting that component with the ZIO effect and then integrate that component with the existing code base, using the unsafeRun function.

Default Runtime

ZIO contains a default runtime called Runtime.default designed to work well for mainstream usage. It is already implemented as below:

object Runtime {
lazy val default: Runtime[Any] = Runtime(ZEnvironment.empty)
}

The default runtime contains minimum capabilities to bootstrap execution of ZIO tasks.

We can easily access the default Runtime to run an effect:

object MainApp extends scala.App {
val myAppLogic = ZIO.succeed(???)
val runtime = Runtime.default
Unsafe.unsafe { implicit unsafe =>
runtime.unsafe.run(myAppLogic).getOrThrowFiberFailure()
}
}

Custom Runtime

Sometimes we need to create a custom Runtime with a user-defined environment.

Some use-cases of custom Runtimes:

Providing Environment to Runtime System

The custom runtime can be used to run many different effects that all require the same environment, so we don't have to call ZIO#provide on all of them before we run them.

For example, assume we want to create a Runtime for services that are for testing purposes, and they don't interact with real external APIs. So we can create a runtime, especially for testing.

Let's say we have defined two LoggingService and EmailService services:

trait LoggingService {
def log(line: String): UIO[Unit]
}

object LoggingService {
def log(line: String): URIO[LoggingService, Unit] =
ZIO.serviceWith[LoggingService](_.log(line))
}

trait EmailService {
def send(user: String, content: String): Task[Unit]
}

object EmailService {
def send(user: String, content: String): ZIO[EmailService, Throwable, Unit] =
ZIO.serviceWith[EmailService](_.send(user, content))
}

We are going to implement a live version of LoggingService and also a fake version of EmailService for testing:

case class LoggingServiceLive() extends LoggingService {
override def log(line: String): UIO[Unit] =
ZIO.succeed(print(line))
}

case class EmailServiceFake() extends EmailService {
override def send(user: String, content: String): Task[Unit] =
ZIO.attempt(println(s"sending email to $user"))
}

Let's create a custom runtime that contains these two service implementations in its environment:

val testableRuntime = Runtime(
ZEnvironment[LoggingService, EmailService](LoggingServiceLive(), EmailServiceFake()),
FiberRefs.empty,
RuntimeFlags.default
)

Also, we can replace the environment of the default runtime with our own custom environment, which allows us to add new services to the ZIO environment:

val testableRuntime: Runtime[LoggingService with EmailService] =
Runtime.default.withEnvironment {
ZEnvironment[LoggingService, EmailService](LoggingServiceLive(), EmailServiceFake())
}

Now we can run our effects using this custom Runtime:

Unsafe.unsafe { implicit unsafe =>
testableRuntime.unsafe.run(
for {
_ <- LoggingService.log("sending newsletter")
_ <- EmailService.send("David", "Hi! Here is today's newsletter.")
} yield ()
).getOrThrowFiberFailure()
}