Resource Management
Both libraries provide robust resource management with guaranteed cleanup.
Basic Resource Pattern
import cats.effect._
import java.io._
// Resource.make(acquire)(release)
val fileResource: Resource[IO, BufferedReader] =
Resource.make(
IO.blocking(new BufferedReader(new FileReader("data.txt")))
)(reader =>
IO.blocking(reader.close()).handleErrorWith(_ => IO.unit)
)
// Use the resource
val content: IO[String] =
fileResource.use { reader =>
IO.blocking(reader.readLine())
}Resource.make - acquire/release pattern
import zio._
import java.io._
// ZIO.acquireRelease(acquire)(release)
val fileScoped: ZIO[Scope, Throwable, BufferedReader] =
ZIO.acquireRelease(
ZIO.attemptBlocking(new BufferedReader(new FileReader("data.txt")))
)(reader =>
ZIO.succeed(reader.close())
)
// Use with scoped
val content: Task[String] = ZIO.scoped {
fileScoped.flatMap { reader =>
ZIO.attemptBlocking(reader.readLine())
}
}ZIO.acquireRelease with Scope
Resource Composition
import cats.effect._
val dbResource: Resource[IO, Database] = ???
val cacheResource: Resource[IO, Cache] = ???
// Compose with for-comprehension
val appResource: Resource[IO, (Database, Cache)] =
for {
db <- dbResource
cache <- cacheResource
} yield (db, cache)
// Both released in reverse order
val program: IO[Unit] =
appResource.use { case (db, cache) =>
IO.println("Using both resources")
}Resource composition via flatMap
import zio._
val dbScoped: ZIO[Scope, Throwable, Database] = ???
val cacheScoped: ZIO[Scope, Throwable, Cache] = ???
// Compose in for-comprehension
val program: Task[Unit] = ZIO.scoped {
for {
db <- dbScoped
cache <- cacheScoped
_ <- ZIO.succeed(println("Using both resources"))
} yield ()
}
// Both released in reverse orderScoped composition via flatMap
Both libraries guarantee cleanup runs in reverse order of acquisition, even if the program fails or is interrupted.
Bracket Pattern
import cats.effect.IO
// bracket(acquire)(release)(use)
val result: IO[String] =
IO.blocking(openConnection())
.bracket(conn => IO.blocking(conn.close())) { conn =>
IO.blocking(conn.fetch())
}bracket - inline resource usage
import zio._
// ZIO.acquireReleaseWith(acquire)(release)(use)
val result: Task[String] =
ZIO.acquireReleaseWith(
ZIO.attemptBlocking(openConnection())
)(conn =>
ZIO.succeed(conn.close())
)(conn =>
ZIO.attemptBlocking(conn.fetch())
)acquireReleaseWith - inline resource usage
fromAutoCloseable
import cats.effect._
import java.io._
// Automatically close AutoCloseable
val reader: Resource[IO, BufferedReader] =
Resource.fromAutoCloseable(
IO.blocking(new BufferedReader(new FileReader("data.txt")))
)Resource.fromAutoCloseable
import zio._
import java.io._
// Automatically close AutoCloseable
val reader: ZIO[Scope, Throwable, BufferedReader] =
ZIO.fromAutoCloseable(
ZIO.attemptBlocking(new BufferedReader(new FileReader("data.txt")))
)ZIO.fromAutoCloseable
Finalizers
import cats.effect.IO
// guarantee - always runs
val withCleanup: IO[Int] =
IO.pure(42)
.guarantee(IO.println("Always runs"))
// guaranteeCase - runs with outcome
val withOutcome: IO[Int] =
IO.pure(42)
.guaranteeCase {
case Outcome.Succeeded(_) => IO.println("Success!")
case Outcome.Errored(e) => IO.println(s"Failed: $$e")
case Outcome.Canceled() => IO.println("Canceled!")
}guarantee / guaranteeCase
import zio._
// ensuring - always runs
val withCleanup: UIO[Int] =
ZIO.succeed(42)
.ensuring(ZIO.succeed(println("Always runs")))
// onExit - runs with exit value
val withOutcome: UIO[Int] =
ZIO.succeed(42)
.onExit {
case Exit.Success(_) => ZIO.succeed(println("Success!"))
case Exit.Failure(cause) => ZIO.succeed(println(s"Failed: $$cause"))
}ensuring / onExit
Resource Allocation
import cats.effect._
// Allocate and get finalizer separately
val allocated: IO[(Database, IO[Unit])] =
dbResource.allocated
// Use then release manually
val program: IO[Unit] = allocated.flatMap { case (db, release) =>
db.query *> release
}allocated - manual resource control
import zio._
// Reserving a resource
val reserved: UIO[Reservation[Any, Throwable, Database]] =
dbScoped.reserve
// Manual acquire/release
val program: Task[Unit] = reserved.flatMap { r =>
r.acquire.flatMap { db =>
db.query *> r.release(Exit.unit)
}
}reserve - manual resource control
The Scope Interface
ZIO 2.x uses the Scope interface as a composable way to manage resource lifecycles. Understanding Scope is key to working with resources in ZIO.
import cats.effect._
// Resource is a self-contained description
// that carries its own finalization logic
val fileResource: Resource[IO, BufferedReader] =
Resource.make(acquireFile)(releaseFile)
// Use with .use - finalization is automatic
val program: IO[String] = fileResource.use { reader =>
IO.blocking(reader.readLine())
}Resource carries finalization with it
import zio._
// Scope interface for resource management
trait Scope {
// Add a finalizer that runs when scope closes
def addFinalizer(finalizer: UIO[Any]): UIO[Unit]
// Add finalizer that can inspect exit value
def addFinalizerExit(
finalizer: Exit[Any, Any] => UIO[Any]
): UIO[Unit]
}
object Scope {
// Create a new closeable scope
def make: UIO[Scope.Closeable]
// Closeable extends Scope with close ability
trait Closeable extends Scope {
def close(exit: Exit[Any, Any]): UIO[Unit]
}
// Run a scoped effect
def scoped[R, E, A](
zio: ZIO[R with Scope, E, A]
): ZIO[R, E, A]
}Scope manages finalizers centrally
The Scope interface separates adding finalizers from closing the scope. This allows you to pass a Scope to other code that can register finalizers without being able to prematurely close the scope.
acquireRelease with Scope
The ZIO.acquireRelease operator uses Scope under the hood:
import cats.effect._
import java.io._
// Resource.make is the primary constructor
val fileResource: Resource[IO, BufferedReader] =
Resource.make(
IO.blocking(new BufferedReader(new FileReader("data.txt")))
)(reader =>
IO.blocking(reader.close()).handleErrorWith(_ => IO.unit)
)Resource.make - self-contained
import zio._
import java.io._
// ZIO.acquireRelease requires Scope in environment
val fileScoped: ZIO[Scope, Throwable, BufferedReader] =
ZIO.acquireRelease(
ZIO.attemptBlocking(new BufferedReader(new FileReader("data.txt")))
)(reader =>
ZIO.succeed(reader.close())
)
// Use with ZIO.scoped
val content: Task[String] = ZIO.scoped {
fileScoped.flatMap { reader =>
ZIO.attemptBlocking(reader.readLine())
}
}ZIO.acquireRelease - requires Scope
Scope-Based Composition
Multiple resources compose naturally with Scope:
import cats.effect._
val dbResource: Resource[IO, Database] = ???
val cacheResource: Resource[IO, Cache] = ???
// Compose with for-comprehension
val appResource: Resource[IO, (Database, Cache)] =
for {
db <- dbResource
cache <- cacheResource
} yield (db, cache)
// Use - both released in reverse order
val program: IO[Unit] =
appResource.use { case (db, cache) =>
db.query *> cache.get
}Resource composition via flatMap
import zio._
val dbScoped: ZIO[Scope, Throwable, Database] = ???
val cacheScoped: ZIO[Scope, Throwable, Cache] = ???
// Compose in for-comprehension
val program: Task[Unit] = ZIO.scoped {
for {
db <- dbScoped
cache <- cacheScoped
_ <- db.query
_ <- cache.get
} yield ()
}
// Both released in reverse order when scope closesScoped composition via flatMap
Cats Effect Resource is a self-contained data structure that carries its finalization logic. ZIO Scope is an environment service that manages finalizers centrally. Both approaches guarantee cleanup runs in reverse order of acquisition.
Next Steps
Resource management is similar in both libraries. Now let's explore fiber supervision.