1. Introduction

Resources are components that are limited in availability, such as database connections, file handles, threads, and so on. Hence, it’s important to properly utilize them and close them after usage to avoid causing resource leakage. ZIO provides a very good mechanism to correctly handle all the resources without leaking them. In this tutorial, let’s look at how ZIO makes it easy to work with resources.

2. Setup

Let’s first add the ZIO dependency to the build.sbt:

libraryDependencies += "dev.zio" %% "zio" % "2.0.3"

3. Resource Handling Using Finalizer

Finalizer is a term generally used in programming to denote some action that’s always executed. We can relate this to the finally block in Java and Scala for exception handling. In ZIO, we can use the method ensuring() to guarantee that an action is always executed, no matter if the previous actions in the chain is succeeded or failed. Let’s look at a simple example:

val simpleZIO = ZIO.succeed {
  println("Creating Connection")
  "con"
} 
val finalizerBlock = ZIO.succeed(println("This is a finalizer!"))
val zioWithFinalizer = simpleZIO.ensuring(finalizerBlock)

We can use ZIOAppDefault to execute the effects:

object ResourceHandling extends ZIOAppDefault {
  override def run = zioWithFinalizer
}

When we now run zioWithFinalizer, we can see the finalizer block print statement in the console:

Creating Connection
This is a finalizer!

Next, let’s create a failure in the ZIO chain to verify that the finalizer is executed:

val failingZIO = ZIO.fail {
  println("Some error occurred")
  -100
}
val complexZIO = simpleZIO *> failingZIO *> ZIO.succeed(println("Final step in chain"))

We have not added any finalizer to the complexZIO. If we execute complexZIO,  we’ll see the following output in the console:

Creating Connection
Some error occurred
timestamp=2022-11-02T19:51:15.025Z level=ERROR thread=#zio-fiber-0 message="" cause="Exception in thread "zio-fiber-4" java.lang.Integer: -100
    at com.baeldung.scala.zio.resources.ResourceHandling.failingZIO(ResourceHandling.scala:13)
    at com.baeldung.scala.zio.resources.ResourceHandling.complexZIO(ResourceHandling.scala:18)"

We can see that the final part of the chain is not printed to the console due to the failure in between. Now, let’s add the finalizer to the mix:

val complexZIOWithFinalizer = complexZIO.ensuring(finalizerBlock)

When we execute complexZIOWithFInalizer, we can see the output in the console:

Creating Connection
Some error occurred
This is a finalizer!
timestamp=2022-11-02T19:55:47.513Z level=ERROR thread=#zio-fiber-0 message="" cause="Exception in thread "zio-fiber-4" java.lang.Integer: -100
    at com.baeldung.scala.zio.resources.ResourceHandling.failingZIO(ResourceHandling.scala:13)
    at com.baeldung.scala.zio.resources.ResourceHandling.complexZIO(ResourceHandling.scala:18)
    at com.baeldung.scala.zio.resources.ResourceHandling.complexZIOWithFinalizer(ResourceHandling.scala:19)"

Even though an error occurred and stopped the ZIO chain in between, the finalizer executed successfully and printed the message “This is a finalizer!”.

4. Resource Handling Using Acquire-And-Release Approach

In the previous section, we looked at finalizers to handle the resources. However, this still doesn’t fully address the issue of missing to release resources after usage. Next, we’ll see how to address that.

4.1. Using acquireReleaseWith()

ZIO provides another way to more safely handle the resources using the method acquireReleaseWith(). This method has three parts: acquire, release and usage. The acquire block provides a way to acquire the required resource, and the release block does the logic to release the resource after usage. The third part, usage block lets us use the acquired resource. So, this enables us to completely decouple the usage part from the acquire and release logic. Since we need to provide all the three blocks to complete the logic, we’ll never miss releasing the resource after usage. Now, let’s look at it with a sample code:

def acquireFile = ZIO.succeed(println("acquiring file")) *> ZIO.succeed("Sauron.txt")
def releaseFile(file: String) = ZIO.succeed(println("Closing file: "+file))
val fileContentZIO = ZIO.acquireReleaseWith(acquireFile)(releaseFile) { file =>
    ZIO.succeed(println("Reading the content from the file: "+file)) *>
    ZIO.succeed("One ring to rule them all!")
}
override def run = fileContentZIO

In the above code, we separated the acquire and release logic into separate methods and combined them in the acquireReleaseWith() method.

4.2. Using acquireRelease()

ZIO also provides another variation where we can first define the logic for the acquire and release. Using the method acquireRelease(), t****he acquired resource can be used separately instead of immediately:

val acquiredResource = ZIO.acquireRelease(acquireFile)(releaseFile)
val acquireReleaseContent = for {
    file <- acquiredResource
    content <- ZIO.succeed(println("Reading from the acquired file: "+file)) *> ZIO.succeed("One ring to rule them all!")
} yield content

ZIO makes sure that the acquire and release blocks are always executed and won’t be interrupted. This way, we can always handle the resources correctly. We should be aware that if the code in the acquire block fails, then the code in the release block won’t get executed. Let’s look at an example:

def acquireFileWithFailure = ZIO.succeed(println("acquiring file")) *> ZIO.succeed("Sauron.txt") *> ZIO.fail("ERROR")
def fileContentFailedAcquire = ZIO.acquireReleaseWith(acquireFileWithFailure)(releaseFile) { file =>
    ZIO.succeed(println("reading from file"))
}

When we execute fileContentFailedAcquire, it prints the output to the console:

acquiring file
timestamp=2022-11-05T08:57:08.573Z level=ERROR thread=#zio-fiber-0 message="" cause="Exception in thread "zio-fiber-4" java.lang.String: ERROR
    at com.baeldung.scala.zio.resources.ResourceHandling.acquireFileWithFailure(ResourceHandling.scala:39)
    at com.baeldung.scala.zio.resources.ResourceHandling.fileContentFailedAcquire(ResourceHandling.scala:40)"

4.3. Nested Resource Behavior

When we perform nested resource handling, the release operation takes place in the reverse order of acquire operation. Let’s look at an example for more clarity:

def acquireDBCon = ZIO.succeed(println("Opening DB Connection")) *> ZIO.succeed("pgsql://localhost:5432")
def releaseDBCon(con: String) = ZIO.succeed(println("Closing DB Connection to URL: "+con))
val nestedResourceZIO = ZIO.acquireReleaseWith(acquireFile)(releaseFile) { file =>
    ZIO.acquireReleaseWith(acquireDBCon)(releaseDBCon) { con =>
      ZIO.succeed(println("Reading the content from the file: "+file + ". Writing to DB: "+con)) *>
      ZIO.succeed("One ring to rule them all AND bring them back!")
    }
}

When we execute nestedResourceZIO, we see the order of the acquire and release operations:

acquiring file
Opening DB Connection
Reading the content from the file: Sauron.txt. Writing to DB: pgsql://localhost:5432
Closing DB Connection to URL: pgsql://localhost:5432
Closing file: Sauron.txt

We should note that the text “One ring to rule them all AND bring them back!” is not printed on the console since we are only wrapping it in the ZIO block.

5. Conclusion

In this article, we looked at different ways to handle resources more safely using ZIO. As always, the sample code shown here is available over on GitHub.