DeferHandle
DeferHandle is a handle returned by Scope.defer that allows cancelling a registered finalizer before the scope closes:
abstract class DeferHandle {
def cancel(): Unit
}
When Scope#defer(cleanup) is called, the cleanup action is registered and a DeferHandle is returned. This handle can be used to remove that finalizer early, preventing it from running when the scope closes. This is useful when a resource is explicitly released before the scope ends, and running the finalizer again would be unnecessary or harmful.
Construction​
DeferHandle is not instantiated directly. Instead, it is created by calling Scope#defer with a cleanup action:
import zio.blocks.scope.{Scope, DeferHandle}
trait Scope {
def defer(cleanup: => Unit): DeferHandle
}
The following example demonstrates creating a DeferHandle:
import zio.blocks.scope.Scope
Scope.global.scoped { scope =>
import scope._
val handle = defer {
println("This cleanup will run when scope closes, unless cancelled")
}
// The handle can now be used to cancel the cleanup
handle.cancel()
}
Core Operations​
The DeferHandle#cancel method removes the registered finalizer so it will not run when the scope closes:
trait DeferHandle {
def cancel(): Unit
}
This method is:
- Thread-safe: Can be called from any thread without synchronization
- Idempotent: Calling it multiple times has the same effect as calling once
If the scope has already closed (and the finalizer has already run or been discarded), calling DeferHandle#cancel is a no-op. In the following example, we register a cleanup action, then cancel it before the scope closes:
import zio.blocks.scope.Scope
import java.io.ByteArrayOutputStream
Scope.global.scoped { scope =>
import scope._
val buffer = allocate(ByteArrayOutputStream())
val closeHandle = defer {
println("Auto-closing buffer")
$(buffer)(_.close())
}
// Manually close the buffer
$(buffer)(_.close())
// Cancel the automatic finalizer since we already closed it
closeHandle.cancel()
}
Use Cases​
DeferHandle is useful in several common scenarios:
Preventing Duplicate Cleanup​
When a resource is explicitly released before the scope ends, cancel the automatic finalizer to avoid duplicate cleanup:
import zio.blocks.scope.Scope
import java.io.ByteArrayOutputStream
val result = Scope.global.scoped { scope =>
import scope._
val buffer = allocate(ByteArrayOutputStream())
val finalizeHandle = defer {
println(s"Finalizer running, buffer closing")
$(buffer)(_.close())
}
// Explicit cleanup
$(buffer) { buf =>
buf.write("data".getBytes)
println(s"Manual use: buffer has ${buf.size()} bytes")
buf.close()
}
// Cancel the automatic finalizer
finalizeHandle.cancel()
"done"
}
Conditional Cleanup​
Cancel finalizers based on runtime conditions:
import zio.blocks.scope.Scope
def acquireResource(shouldCleanup: Boolean) = Scope.global.scoped { scope =>
import scope._
val resource = "important resource"
val handle = scope.defer {
println("Cleaning up resource")
}
if (!shouldCleanup) {
handle.cancel()
println("Cleanup disabled")
}
resource
}
acquireResource(shouldCleanup = false)
acquireResource(shouldCleanup = true)
Transferring Ownership​
When transferring a resource to external management, cancel its finalizer so the external system can control cleanup:
import zio.blocks.scope.Scope
import java.io.ByteArrayInputStream
val result = Scope.global.scoped { scope =>
import scope._
val stream = allocate(ByteArrayInputStream("data".getBytes))
val handle = defer {
println("Scope finalizer would close stream")
$(stream)(_.close())
}
// Transfer ownership to external manager
// (In real code, this might pass to a thread pool or async framework)
handle.cancel() // Let the manager handle cleanup
"transferred"
}
Noop Handle​
When defer() is called on an already-closed scope, a no-op handle is returned:
import zio.blocks.scope.Scope
Scope.global.scoped { scope =>
import scope._
val handle = scope.defer {
println("This will run when scope closes")
}
// Subsequent calls to cancel() remove the finalizer
handle.cancel()
println("Finalizer has been cancelled")
}
Thread Safety​
DeferHandle is thread-safe. Multiple threads can call cancel() on the same handle without external synchronization:
import zio.blocks.scope.Scope
import java.util.concurrent.CountDownLatch
import java.util.concurrent.Executors
val result = Scope.global.scoped { scope =>
import scope._
val handle = defer {
println("Finalizer")
}
// Use a thread pool to simulate concurrent access
val executor = Executors.newFixedThreadPool(5)
val latch = new CountDownLatch(5)
(1 to 5).foreach { i =>
executor.submit(new Runnable {
def run(): Unit = {
handle.cancel()
println(s"Thread $i cancelled")
latch.countDown()
}
})
}
// Wait for all threads to finish
latch.await()
executor.shutdown()
"completed"
}
See Also​
Scope#defer— the method that returns aDeferHandleFinalizer— the trait definingFinalizer#deferFinalization— the result of running all finalizers
Integration​
DeferHandle is part of ZIO Blocks' resource management system. It works directly with:
-
Scope— The primary way to create aDeferHandleis viaScope#defer. A scope manages multiple finalizers and runs them all when the scope closes.DeferHandleallows selective cancellation of individual finalizers before that happens. -
Finalizer—Finalizerdefines theFinalizer#deferoperation that returns aDeferHandle. It abstracts the concept of registering cleanup actions. -
Finalization— When a scope closes, it runs all registered finalizers. A cancelledDeferHandleremoves its associated finalizer from this process.
Together, these types form the foundation of compile-time resource safety in ZIO Blocks, allowing you to manage resource lifecycles with certainty that cleanup will occur exactly when needed.