Scope
zio.blocks.scope is a compile-time safe, zero-cost resource management library for Scala 3 (and Scala 2.13). It prevents a large class of lifetime bugs by tagging allocated values with an unnameable, scope-specific type and restricting how those values may be used.
At runtime the model stays simple:
- Allocate eagerly (no lazy thunks)
- Register finalizers
- Run finalizers deterministically when a scope closes (LIFO order)
- Collect finalizer failures into a
Finalization(and throw/suppress appropriately)
Why Scope?
Most resource bugs in Scala are "escape" bugs:
- storing a connection/stream in a field and using it after it was closed
- capturing a resource in a closure that outlives a scope
- passing a resource to code that might retain it
- mixing values from different lifetimes ("which scope owns this?")
Scope addresses these with a tight design:
| Feature | zio.blocks.scope |
|---|---|
| Compile-time leak prevention | ✓ (scope.$[A] + $ macro + Unscoped boundary) |
| Runtime overhead | ~0 (scoped values erase to A) |
| Allocation model | Eager (allocation happens at allocate) |
| Finalization | Deterministic, LIFO, errors collected |
| Structured lifetime | Parent/child scopes, lower for explicit lifetime widening |
| Escape hatch | leak (warns) |
If you've used try/finally, Using, or ZIO's Scope, this is the same problem space—but optimized for synchronous code with compile-time boundaries.
Quick start (Scala 3)
import zio.blocks.scope.*
final class Database extends AutoCloseable:
def query(sql: String): String = s"result: $sql"
def close(): Unit = println("db closed")
@main def quickStart(): Unit =
val out: String =
Scope.global.scoped { scope =>
import scope.*
val db: $[Database] =
Resource.fromAutoCloseable(new Database).allocate
// Safe access: the lambda parameter can only be used as a receiver
$(db)(_.query("SELECT 1"))
}
println(out)
Key points:
allocate(...)returns a scoped value:scope.$[Database](or$[Database]afterimport scope.*).- You cannot call
db.query(...)directly on$[Database]. - You use the
$access operator:$(db)(...)(or(scope $ db)(...)without the import). - The
scopedblock returns a plainStringbecauseString: Unscoped. - Finalizers run when the block exits, in LIFO order.
Core mental model
1) Scope: finalizers + type identity
Scope is a finalizer registry plus a unique type identity:
type $[+A]— a scope-tagged, path-dependent type (erases toAat runtime)type Parent <: Scope/val parent: Parent— the scope hierarchy
Every scope instance defines a different $ type, so values from different scopes don't accidentally mix.
Scope.global.scoped { scope =>
import scope.*
val x: $[Int] = 1 // ok (in global, $[A] = A)
}
Global scope
Scope.global is the root:
- In the global scope:
type $[+A] = A(identity) - On the JVM: global finalizers run on shutdown via a shutdown hook
- On Scala.js: there is no shutdown hook, so global finalizers are not run automatically
2) Scoped values: scope.$[A] / $[A]
A value of type scope.$[A] means:
"This is an
A, but it is only valid whilescopeis alive."
Properties:
- Zero-cost:
$[A]is justAat runtime (casts/identity) - Incompatible across scopes:
outer.$[A]is notinner.$[A] - Methods are hidden at the type level; you must use
$to access
Access operator: (scope $ value)(f)
The intended way to use a scoped value is:
(scope $ scopedValue)(a => a.method(...))
This is enforced by a macro that checks the lambda uses its parameter only in receiver position.
Allowed:
(scope $ db)(_.query("SELECT 1"))
(scope $ db)(d => d.query("a") + d.query("b"))
(scope $ db)(_.query("x").toUpperCase)
(scope $ db)(_.field) // field access is allowed
Rejected at compile time:
(scope $ db)(d => store(d)) // parameter used as an argument
(scope $ db)(d => () => d.query("x")) // captured in a nested lambda
(scope $ db)(d => d) // returning the parameter
(scope $ db)(d => { val x = d; 1 }) // binding/storing the parameter itself
"Auto-unwrap" rule (Unscoped)
$ auto-unwraps when the result type is known to be safe data:
- if
B: Unscoped→(scope $ sa)(f)returnsB - otherwise → it returns
scope.$[B]
Scope.global.scoped { scope =>
import scope.*
val db: $[Database] = Resource.from[Database].allocate
val s: String = $(db)(_.query("SELECT 1")) // String is Unscoped => unwrapped
val n: Int = $(db)(_.query("x").length) // Int is Unscoped => unwrapped
}
N-ary $: accessing multiple scoped values at once
When a result depends on two or more scoped values simultaneously, use the N-ary overloads (N = 2..5):
$(sa1, sa2)((v1, v2) => v1.method(v2.result()))
$(sa1, sa2, sa3)((v1, v2, v3) => v1.query(v2.key()) + v3.tag())
The same receiver-only grammar applies to every parameter: each vi may only appear as a method receiver (e.g., vi.method()). Feeding the result of one parameter to a method of another is permitted:
Scope.global.scoped { scope =>
import scope.*
val db: $[Database] = Resource.from[Database].allocate
val cache: $[Cache] = Resource.from[Cache].allocate
// d1 and d2 are both receivers; d2.key() produces a plain String arg
val result: String = $(db, cache)((d1, d2) => d1.query(d2.key()))
}
Rejected at compile time (same rules as N=1, applied to each parameter independently):
$(db, cache)((d1, d2) => d2) // d2 returned directly
$(db, cache)((d1, d2) => store(d1)) // d1 passed as argument
$(db, cache)((d1, d2) => d1.method(d2)) // d2 as bare arg (not a receiver)
$(db, cache)((d1, d2) => () => d2.query()) // d2 captured in closure
The error messages name the offending parameter:
Parameter 2 ('d2') cannot be passed as an argument to a function or method.
Scoped values may only be used as a method receiver (e.g., d2.method()).
Infix syntax (scope $ sa) is only available for N=1. For N≥2, use unqualified syntax after import scope.*:
$(db, cache)((d, c) => d.query(c.key())) // ✓ unqualified
For N>5, extract each value in sequence (all results are Unscoped strings/values and can be freely combined):
val q1 = $(db1)(_.query("a"))
val q2 = $(db2)(_.query("b"))
q1 + q2
3) Resource[A]: acquisition + finalization
A Resource[A] is a lazy description of how to acquire a value and register cleanup in a scope. Nothing happens until you call scope.allocate(resource) (or .allocate syntax).
Constructors
From the source:
Resource(value: => A)Wraps a by-name value; if it'sAutoCloseable,close()is registered automatically (runtime check).Resource.fromAutoCloseable(thunk: => A <: AutoCloseable)Type-safe helper that registersclose().Resource.acquireRelease(acquire: => A)(release: A => Unit)Resource.shared(f: Scope => A)Memoized + reference-counted, thread-safe.Resource.unique(f: Scope => A)Fresh instance per allocation.Resource.from[T]andResource.from[T](wires*)(macros) Constructor-based dependency injection (covered below).
Composition
Resource composes with:
mapflatMapzip
Finalizers remain tied to the allocation scope; in composed resources, finalizers still run LIFO.
Sharing vs uniqueness (important)
There are two distinct ideas:
-
Uniqueness: "each allocation yields a fresh instance"
- Use
Resource.unique(...), or most ordinaryResource(...)/acquireRelease(...)resources. - Each
allocateruns the acquisition again and registers an independent finalizer.
- Use
-
Sharing: "reusing the same instance across multiple allocations"
- Use
Resource.shared(...)(or wires/resources that convert to shared). - Sharing is tied to reusing the same
Resource.Sharedvalue, not "magic caching inside a scope". - The first allocation initializes via an
OpenScopeparented toScope.global; subsequent allocations increment a reference count. When the last referencing scope closes, the shared scope is closed.
- Use
4) Unscoped[A]: types that may escape a scope
Unscoped[A] is a marker typeclass for pure data. It's used in two places:
Scope.scopedrequiresUnscoped[A]for the block's result type ⇒ prevents returning resources, closures, or scoped values.$auto-unwraps results of typeBwhenB: Unscoped.
Built-in instances include primitives, String, many collections/containers, time values, java.util.UUID, and zio.blocks.chunk.Chunk (when element types are unscoped).
Deriving / defining your own instances
Scala 3 (derivation via Unscoped.derived):
import zio.blocks.scope.*
final case class Config(debug: Boolean)
object Config:
given Unscoped[Config] = Unscoped.derived
Scala 2.13:
import zio.blocks.scope.*
final case class Config(debug: Boolean)
object Config {
implicit val unscopedConfig: Unscoped[Config] = Unscoped.derived[Config]
}
Scope boundary example
import zio.blocks.scope.*
Scope.global.scoped { parent =>
import parent.*
val ok: String =
parent.scoped { child =>
"hello" // String is Unscoped
}
// Does not compile: returning a resourceful value from a scoped block
// val leaked: Database =
// parent.scoped { child =>
// import child.*
// Resource.fromAutoCloseable(new Database).allocate
// }
ok
}
5) lower: using a parent-scoped value in a child scope
Because each scope has its own $[A] type, a child cannot directly use a parent's $[A]. Use lower to retag a parent-scoped value into the child:
import zio.blocks.scope.*
Scope.global.scoped { outer =>
import outer.*
val db: $[Database] = Resource.fromAutoCloseable(new Database).allocate
outer.scoped { inner =>
import inner.*
val innerDb: $[Database] = lower(db)
$(innerDb)(_.query("child"))
}
}
This is safe because parents always outlive children (child finalizers run before the parent closes).
6) defer: manual finalizers (+ cancellation)
Use defer to register cleanup. It returns a DeferHandle you can cancel.
import zio.blocks.scope.*
Scope.global.scoped { scope =>
import scope.*
val in = new java.io.ByteArrayInputStream(Array[Byte](1, 2, 3))
val h: DeferHandle =
defer(in.close())
val first = in.read()
println(first)
// If you already cleaned up manually:
// h.cancel() // thread-safe, idempotent
}
There is also a package-level helper that only requires a Finalizer:
import zio.blocks.scope.*
Scope.global.scoped { scope =>
import scope.*
given Finalizer = scope
defer(println("cleanup")) // uses the package-level helper
}
7) open(): non-lexical, explicitly-managed child scopes
scoped ties lifetime to a block. open() creates a child scope you close explicitly.
- The child scope is unowned (can be used from any thread)
- Still linked to the parent: parent closing will also close the child
- You must call
close()on the handle to detach + finalize now
From Scope.global the returned type is Scope.OpenScope directly (because global $[A] = A):
import zio.blocks.scope.*
val os: Scope.OpenScope = Scope.global.open()
val db = os.scope.allocate(Resource.fromAutoCloseable(new Database))
// ... use db ...
os.close().orThrow()
Inside a child scope, open() returns $[Scope.OpenScope]. Prefer using it safely via $:
import zio.blocks.scope.*
Scope.global.scoped { parent =>
import parent.*
val os: $[Scope.OpenScope] = open()
$(os) { h =>
val child = h.scope
val db = child.allocate(Resource.fromAutoCloseable(new Database))
// ...
h.close().orThrow()
}
}
8) Escape hatch: leak
Sometimes you must hand a raw value to code that cannot work with $[A]. Use leak:
import zio.blocks.scope.*
Scope.global.scoped { scope =>
import scope.*
val db: $[Database] = Resource.fromAutoCloseable(new Database).allocate
val raw: Database = leak(db) // emits a compiler warning
// thirdParty(raw)
}
leak bypasses compile-time guarantees—use only for unavoidable interop. If the type is genuinely pure data, prefer adding Unscoped so you don't need to leak.
Safety model (why leaking is prevented)
Scope's safety comes from three reinforcing layers.
1) Type barrier: scope-specific $[A]
Every scope has a distinct $[A] type. You cannot accidentally use values across scopes without an explicit conversion (lower for parent → child).
2) Controlled access: $ macro restricts lambda usage
The $ operator only allows using the unwrapped value as a method/field receiver. This prevents:
- returning the resource
- storing it in a local val/var
- passing it as an argument
- capturing it in a closure
Also note: $ requires a lambda literal. Method references / variables are rejected:
// does not compile:
val f: Database => String = _.query("x")
(scope $ db)(f) // "$ requires a lambda literal ..."
3) Scope boundary rule: scoped requires Unscoped[A]
A scoped { ... } block can only return pure data (or Nothing). Resources and closures cannot escape.
Pragmatic safety. The type-level tagging prevents accidental scope misuse in normal code, but it is not a security boundary. A determined developer can bypass it via leak (which emits a compiler warning), unsafe casts (asInstanceOf), or storing scoped references in mutable state (var).
Closed-scope safety (runtime)
If a scope reference escapes its scoped { } block and an operation is attempted after closing, Scope throws IllegalStateException with a detailed, actionable error message:
-
allocateon a closed scope:── Scope Error ─────────────────────────────────────────────────────────────────
Cannot allocate resource: scope is already closed.
Scope: Scope.Child
What happened:
A call to allocate was made on a scope whose finalizers have
already run. The resource was never acquired.
Common causes:
• A scope reference escaped a scoped { } block (e.g. stored in a
field, captured in a Future or passed to another thread).
• close() was called on an OpenScope before all
allocations inside it completed.
Fix:
Call allocate only inside a live scoped { } block, or before
calling close() on an OpenScope.
// Correct usage:
Scope.global.scoped { scope =>
import scope.*
val db = allocate(Resource(new Database))
$(db)(_.query("SELECT 1"))
}
──────────────────────────────────────────────────────────────────────────────── -
open()on a closed scope gives the same treatment, explaining that no child scope was created and directing the user to callopen()only on a live scope. -
$on a closed scope explains that the resource may have already been released and accessing it would be undefined behaviour.
The following operations on a closed scope do not throw:
defer— silently ignored (no-op)scoped— runs normally but creates a born-closed child scopelower— zero-cost cast, no closed check needed
Thread ownership rule (JVM)
- Scopes created by
scopedare owned by the entering thread. - Calling
scopedon a scope you don't own throwsIllegalStateException. open()creates an unowned child scope (isOwner == truefrom any thread).
(Scala.js uses a trivial ownership model; isOwner is effectively always true.)
Usage examples (patterns)
Allocating and using a resource
import zio.blocks.scope.*
final class FileHandle(path: String) extends AutoCloseable:
def readAll(): String = s"contents of $path"
def close(): Unit = println(s"closed $path")
@main def fileExample(): Unit =
Scope.global.scoped { scope =>
import scope.*
val h: $[FileHandle] =
Resource(new FileHandle("data.txt")).allocate
val contents: String =
$(h)(_.readAll())
println(contents)
}
Nested scopes (child can use parent, not vice versa)
import zio.blocks.scope.*
final class Database extends AutoCloseable:
def query(sql: String): String = s"result: $sql"
def close(): Unit = println("db closed")
@main def nested(): Unit =
Scope.global.scoped { parent =>
import parent.*
val parentDb: $[Database] = Resource.fromAutoCloseable(new Database).allocate
val done: String =
parent.scoped { child =>
import child.*
val db: $[Database] = lower(parentDb)
println($(db)(_.query("SELECT 1")))
val childDb: $[Database] = Resource.fromAutoCloseable(new Database).allocate
println($(childDb)(_.query("SELECT 2")))
// childDb cannot be returned to the parent (not Unscoped)
"done"
}
println($(parentDb)(_.query("SELECT 3")))
done
}
Finalizers run child first, then parent.
Chaining resource acquisition ($[Resource[A]] + .allocate)
If a method returns Resource[A], $ returns a scoped Resource[A] (because Resource[A] is not Unscoped). Allocate it without leaking:
import zio.blocks.scope.*
final class Pool extends AutoCloseable:
def lease(): Resource[Conn] = Resource.fromAutoCloseable(new Conn)
def close(): Unit = println("pool closed")
final class Conn extends AutoCloseable:
def query(sql: String): String = s"result: $sql"
def close(): Unit = println("connection closed")
@main def chaining(): Unit =
Scope.global.scoped { scope =>
import scope.*
val pool: $[Pool] = Resource.fromAutoCloseable(new Pool).allocate
// $(pool)(_.lease()) : $[Resource[Conn]]
val conn: $[Conn] =
$(pool)(_.lease()).allocate
val result: String =
$(conn)(_.query("SELECT 1"))
println(result)
}
This .allocate comes from Scope.ScopedResourceOps (an extension on $[Resource[A]]).
Allocating a bare Resource[A] with .allocate
A plain Resource[A] also has .allocate as syntax sugar for scope.allocate(resource):
import zio.blocks.scope.*
Scope.global.scoped { scope =>
import scope.*
val db: $[Database] =
Resource.fromAutoCloseable(new Database).allocate
$(db)(_.query("SELECT 1"))
}
Classes with Finalizer parameters (cleanup-only capability)
If a class only needs cleanup registration, accept a Finalizer. DI macros inject it automatically.
import zio.blocks.scope.*
final case class Config(url: String)
object Config:
given Unscoped[Config] = Unscoped.derived
final class ConnectionPool(config: Config)(using Finalizer):
private val pool = s"pool(${config.url})"
defer(println(s"shutdown $pool"))
val poolResource: Resource[ConnectionPool] =
Resource.from[ConnectionPool](
Wire(Config("jdbc://localhost"))
)
@main def finalizerInjection(): Unit =
Scope.global.scoped { scope =>
import scope.*
val pool: $[ConnectionPool] = poolResource.allocate
()
}
When to prefer Finalizer over Scope:
- you only need
defer - you want to expose minimal power to the class
Classes with Scope parameters (scope injection)
If a class needs to allocate resources or create child scopes, accept a Scope:
import zio.blocks.scope.*
final case class Config(url: String)
object Config:
given Unscoped[Config] = Unscoped.derived
final class Connection(config: Config) extends AutoCloseable:
def query(sql: String): String = s"[${config.url}] $sql"
def close(): Unit = println("connection closed")
final class RequestHandler(config: Config)(using scope: Scope):
def handle(sql: String): String =
scope.scoped { child =>
import child.*
val conn: $[Connection] = Resource.fromAutoCloseable(new Connection(config)).allocate
$(conn)(_.query(sql))
}
val handlerResource: Resource[RequestHandler] =
Resource.from[RequestHandler](
Wire(Config("jdbc://localhost"))
)
@main def scopeInjection(): Unit =
Scope.global.scoped { scope =>
import scope.*
val handler: $[RequestHandler] = handlerResource.allocate
val out: String = $(handler)(_.handle("SELECT 1"))
println(out)
}
The Scope/Finalizer parameter can appear in any parameter list position; it's recognized specially by the derivation macros.
Dependency injection (DI) with Wire + Resource.from
Scope includes a small constructor-based DI layer built on top of zio.blocks.context.Context. For a comprehensive guide to Wire and its construction patterns, see the Wire reference page.
Wire[-In, +Out]: a dependency recipe
A Wire is a recipe for constructing Out from a Context[In] (and a Scope for finalization):
Wire.Shared→ converts toResource.shared(ref-counted sharing)Wire.Unique→ converts toResource.unique(fresh instance)
Manual wire + Context
import zio.blocks.scope.*
import zio.blocks.context.Context
final case class Config(debug: Boolean)
object Config:
given Unscoped[Config] = Unscoped.derived
val w: Wire.Shared[Boolean, Config] =
Wire.shared[Config] // Boolean => Config
val deps: Context[Boolean] =
Context(true)
@main def wireAndContext(): Unit =
Scope.global.scoped { scope =>
import scope.*
val cfg: $[Config] =
allocate(w.toResource(deps))
val debug: Boolean =
$(cfg)(_.debug)
println(debug)
}
Sharing vs uniqueness at the wire level
import zio.blocks.scope.*
val ws = Wire.shared[Config] // shared recipe
val wu = Wire.unique[Config] // unique recipe
The difference is realized when converting to resources (toResource) and allocating.
Resource.from[T](wires*): derive a whole object graph
Resource.from[T](wires*) is the primary entry point for DI. It:
- uses provided wires as overrides
- auto-creates missing wires for concrete classes (defaulting to shared)
- rejects unmakeable/abstract types unless you provide a wire
- detects cycles, duplicate providers, and subtype conflicts
- generates a composed
Resource[T]viaflatMapchains (preserving sharing/uniqueness)
Example:
import zio.blocks.scope.*
final case class Config(url: String)
object Config:
given Unscoped[Config] = Unscoped.derived
final class Logger:
def info(msg: String): Unit = println(msg)
final class Database(cfg: Config) extends AutoCloseable:
def query(sql: String): String = s"[${cfg.url}] $sql"
def close(): Unit = println("database closed")
final class Service(db: Database, logger: Logger) extends AutoCloseable:
def run(): Unit = logger.info(s"running with ${db.query("SELECT 1")}")
def close(): Unit = println("service closed")
val serviceResource: Resource[Service] =
Resource.from[Service](
Wire(Config("jdbc:postgresql://localhost/db")) // leaf value
)
@main def di(): Unit =
Scope.global.scoped { scope =>
import scope.*
val svc: $[Service] = serviceResource.allocate
$(svc)(_.run())
}
Injecting traits via subtype wires
When a dependency is abstract, provide a wire for a concrete implementation:
import zio.blocks.scope.*
trait Logger:
def info(msg: String): Unit
final class ConsoleLogger extends Logger:
def info(msg: String): Unit = println(msg)
final class App(logger: Logger):
def run(): Unit = logger.info("Hello!")
val appResource: Resource[App] =
Resource.from[App](
Wire.shared[ConsoleLogger] // satisfies Logger via subtyping
)
@main def traitInjection(): Unit =
Scope.global.scoped { scope =>
import scope.*
val app: $[App] = appResource.allocate
$(app)(_.run())
}
Diamond patterns share a single instance (when appropriate)
import zio.blocks.scope.*
trait Service
final class LiveService extends Service
final class NeedsService(s: Service)
final class NeedsLive(l: LiveService)
final class App(a: NeedsService, b: NeedsLive)
val appResource: Resource[App] =
Resource.from[App](
Wire.shared[LiveService]
)
// LiveService instantiations: 1
Common runtime errors (and what they mean)
These IllegalStateExceptions are thrown when a scope operation is attempted on a closed scope. Each message identifies the scope type, explains what went wrong, lists common causes, and shows a correct usage example.
allocate on a closed scope
── Scope Error ─────────────────────────────────────────────────────────────────
Cannot allocate resource: scope is already closed.
Scope: Scope.Child
What happened:
A call to allocate was made on a scope whose finalizers have
already run. The resource was never acquired.
Common causes:
• A scope reference escaped a scoped { } block (e.g. stored in a
field, captured in a Future or passed to another thread).
• close() was called on an OpenScope before all
allocations inside it completed.
Fix:
Call allocate only inside a live scoped { } block, or before
calling close() on an OpenScope.
// Correct usage:
Scope.global.scoped { scope =>
import scope.*
val db = allocate(Resource(new Database))
$(db)(_.query("SELECT 1"))
}
────────────────────────────────────────────────────────────────────────────────
open() on a closed scope
── Scope Error ─────────────────────────────────────────────────────────────────
Cannot open child scope: scope is already closed.
Scope: Scope.Child
What happened:
A call to open() was made on a scope whose finalizers have
already run. No child scope was created.
Common causes:
• A scope reference escaped a scoped { } block and open()
was called after the block exited.
• close() was called on the parent OpenScope before
open() was called on it.
Fix:
Call open() only on a live (not yet closed) scope.
// Correct usage:
Scope.global.scoped { scope =>
import scope.*
val child = open()
$(child)(_.scope.allocate(Resource(new Database)))
}
────────────────────────────────────────────────────────────────────────────────
$ on a closed scope
── Scope Error ─────────────────────────────────────────────────────────────────
Cannot access scoped value: scope is already closed.
Scope: Scope.Child
What happened:
The $ operator was called on a scope whose finalizers have
already run. The underlying resource may have been released.
Accessing it would be undefined behavior.
Common causes:
• A $[A] value or its owning scope escaped a scoped { }
block (e.g. captured in a Future, stored in a field, or
passed to another thread).
• close() was called on an OpenScope that still has
live $[A] values being accessed.
Fix:
Ensure all $ calls occur strictly within the scoped { }
block that owns the value, and that the scope has not been closed.
// Correct usage:
Scope.global.scoped { scope =>
import scope.*
val db = allocate(Resource(new Database))
$(db)(_.query("SELECT 1")) // $ used inside the block
}
────────────────────────────────────────────────────────────────────────────────
Common compile errors (and what they mean)
This module produces two kinds of compile-time feedback:
- Plain macro aborts for unsafe
$usage - ASCII-rendered errors/warnings for DI derivation + leak warnings (via
internal.ErrorMessages)
Unsafe use inside $
All messages name the offending parameter by its 1-based index and source name, and end with the receiver-only reminder. Typical messages:
Parameter 1 ('d') cannot be passed as an argument to a function or method.
Scoped values may only be used as a method receiver (e.g., d.method()).
Parameter 1 ('d') must only be used as a method receiver.
It cannot be returned, stored, passed as an argument, or captured.
Scoped values may only be used as a method receiver (e.g., d.method()).
Parameter 1 ('d') cannot be captured in a nested lambda, def, or anonymous class.
Scoped values may only be used as a method receiver (e.g., d.method()).
Parameter 2 ('cache') cannot be passed as an argument to a function or method.
Scoped values may only be used as a method receiver (e.g., cache.method()).
$ requires a lambda literal, e.g. $(x)(a => a.method()).
Method references and variables are not supported.
Not a class (Wire.shared/unique on a trait / abstract)
── Scope Error ─────────────────────────────────────────────────────────────────
Cannot derive Wire for MyTrait: not a class.
Hint: Use Wire.Shared / Wire.Unique directly.
───────────────────────────────────────────────────────────────────────────────
No primary constructor
── Scope Error ─────────────────────────────────────────────────────────────────
MyType has no primary constructor.
Hint: Use Wire.Shared / Wire.Unique directly
with a custom construction strategy.
───────────────────────────────────────────────────────────────────────────────
Resource.from[T] used when T has dependencies
── Scope Error ─────────────────────────────────────────────────────────────────
Resource.from[MyService] cannot be derived.
MyService has dependencies that must be provided:
• Config
• Logger
Hint: Use Resource.from[MyService](wire1, wire2, ...)
to provide wires for all dependencies.
───────────────────────────────────────────────────────────────────────────────
Unmakeable type (primitives, functions, collections)
── Scope Error ─────────────────────────────────────────────────────────────────
Cannot auto-create String
This type (primitive, collection, or function) cannot be auto-created.
Required by:
├── Config
└── App
Fix: Provide Wire(value) with the desired value:
Resource.from[...](
Wire(...), // provide a value for String
...
)
───────────────────────────────────────────────────────────────────────────────
Abstract type (trait / abstract class dependency)
── Scope Error ─────────────────────────────────────────────────────────────────
Cannot auto-create Logger
This type is abstract (trait or abstract class).
Required by:
└── App
Fix: Provide a wire for a concrete implementation:
Resource.from[...](
Wire.shared[ConcreteImpl], // provides Logger
...
)
───────────────────────────────────────────────────────────────────────────────
Duplicate providers (ambiguous wires)
── Scope Error ────────────────────────────────────────────────────────────────
Multiple providers for Service
Conflicting wires:
1. LiveService
2. TestService
Hint: Remove duplicate wires or use distinct wrapper types.
───────────────────────────────────────────────────────────────────────────────
Dependency cycle
── Scope Error ────────────────────────────────────────────────────────────────
Dependency cycle detected
Cycle:
┌───────────┐
│ ▼
A ──► B ──► C
▲ │
└───────────┘
Break the cycle by:
• Introducing an interface/trait
• Using lazy initialization
• Restructuring dependencies
───────────────────────────────────────────────────────────────────────────────
Subtype conflict (related dependency types)
── Scope Error ────────────────────────────────────────────────────────────────
Dependency type conflict in MyService
FileInputStream is a subtype of InputStream.
When both types are dependencies, Context cannot reliably distinguish
them. The more specific type may be retrieved when the more general
type is requested.
To fix this, wrap one or both types in a distinct wrapper:
case class WrappedInputStream(value: InputStream)
or
opaque type WrappedInputStream = InputStream
───────────────────────────────────────────────────────────────────────────────
Duplicate parameter types in a constructor
── Scope Error ────────────────────────────────────────────────────────────────
Constructor of App has multiple parameters of type String
Context is type-indexed and cannot supply distinct values for the same type.
Fix: Wrap one parameter in an opaque type to distinguish them:
opaque type FirstString = String
or
case class FirstString(value: String)
───────────────────────────────────────────────────────────────────────────────
Leak warning
── Scope Warning ───────────────────────────────────────────────────────────────
leak(db)
^
|
Warning: db is being leaked from scope zio.blocks.scope.Scope.Child[...].
This may result in undefined behavior.
Hint:
If you know this data type is not resourceful, then add an Unscoped
instance for it so you do not need to leak it.
───────────────────────────────────────────────────────────────────────────────
API reference (from source)
Examples below use Scala 3 syntax. Scala 2.13 has equivalent APIs, but macro signatures differ slightly (notably $'s return type encoding).
Scope
sealed abstract class Scope extends Finalizer with ScopeVersionSpecific
Associated types and hierarchy:
type $[+A]type Parent <: Scopeval parent: Parentdef isClosed: Booleandef isOwner: Boolean
Core operations:
def scoped[A](f: (child: Scope.Child[this.type]) => A)(using Unscoped[A]): A
def allocate[A](resource: Resource[A]): $[A]
def allocate[A <: AutoCloseable](value: => A): $[A]
// N=1 (infix available: `scope $ sa`)
infix transparent inline def $[A, B](sa: $[A])(inline f: A => B): B | $[B]
// N=2..5 (unqualified syntax: `$(sa1, sa2)(f)` after `import scope.*`)
transparent inline def $[A1, A2, B](sa1: $[A1], sa2: $[A2])(inline f: (A1, A2) => B): B | $[B]
transparent inline def $[A1, A2, A3, B](sa1: $[A1], sa2: $[A2], sa3: $[A3])(inline f: (A1, A2, A3) => B): B | $[B]
transparent inline def $[A1, A2, A3, A4, B](sa1: $[A1], sa2: $[A2], sa3: $[A3], sa4: $[A4])(inline f: (A1, A2, A3, A4) => B): B | $[B]
transparent inline def $[A1, A2, A3, A4, A5, B](sa1: $[A1], sa2: $[A2], sa3: $[A3], sa4: $[A4], sa5: $[A5])(inline f: (A1, A2, A3, A4, A5) => B): B | $[B]
def lower[A](value: parent.$[A]): $[A]
override def defer(f: => Unit): DeferHandle
def open(): $[Scope.OpenScope]
inline def leak[A](inline sa: $[A]): A
Notes:
$(all arities) requires a lambda literal and enforces safe receiver-only usage at compile time.$returnsBifUnscoped[B]exists; otherwise returns$[B].- N=1 is
infix; N≥2 are not — use unqualified syntax afterimport scope.*. - For N>5, call
$once per resource and combine the resulting plain (Unscoped) values. - If the scope is closed,
$,allocate, andopenthrowIllegalStateExceptionwith a detailed error message.deferandlowerare unaffected.
Syntax enrichments available after import scope.* inside a scope:
implicit class ScopedResourceOps[A](sr: $[Resource[A]]):
def allocate: $[A]
implicit class ResourceOps[A](r: Resource[A]):
def allocate: $[A]
Scope.global
object Scope:
object global extends Scope
Properties:
type $[+A] = A(identity)isOwneralways returnstrue- JVM: finalizers run at shutdown via a shutdown hook
- Scala.js: shutdown hook is not available
Scope.OpenScope
case class OpenScope(scope: Scope, close: () => Finalization)
scope: the child scopeclose(): detaches from parent, runs child finalizers (LIFO), returnsFinalization
Finalizer
trait Finalizer:
def defer(f: => Unit): DeferHandle
A minimal capability interface for registering cleanup.
Also available as a package-level helper:
def defer(finalizer: => Unit)(using fin: Finalizer): DeferHandle
DeferHandle
abstract class DeferHandle:
def cancel(): Unit
cancel()is thread-safe and idempotent- cancellation is O(1) (true removal from a concurrent map)
Finalization
final class Finalization(val errors: zio.blocks.chunk.Chunk[Throwable]):
def isEmpty: Boolean
def nonEmpty: Boolean
def orThrow(): Unit
def suppress(initial: Throwable): Throwable
object Finalization:
val empty: Finalization
def apply(errors: Chunk[Throwable]): Finalization
Resource[+A]
sealed trait Resource[+A]:
def map[B](f: A => B): Resource[B]
def flatMap[B](f: A => Resource[B]): Resource[B]
def zip[B](that: Resource[B]): Resource[(A, B)]
Companion constructors:
object Resource:
def apply[A](value: => A): Resource[A]
def fromAutoCloseable[A <: AutoCloseable](thunk: => A): Resource[A]
def acquireRelease[A](acquire: => A)(release: A => Unit): Resource[A]
def shared[A](f: Scope => A): Resource[A]
def unique[A](f: Scope => A): Resource[A]
inline def from[T]: Resource[T]
inline def from[T](inline wires: Wire[?, ?]*): Resource[T]
Notes:
Resource.from[T](no args) only works whenThas no non-scope dependencies (constructor params may includeScope/Finalizer).- Use
Resource.from[T](wires*)to provide/override dependencies and derive the full graph.
Wire[-In, +Out]
sealed trait Wire[-In, +Out]:
def isShared: Boolean
def isUnique: Boolean = !isShared
def shared: Wire.Shared[In, Out]
def unique: Wire.Unique[In, Out]
def toResource(deps: zio.blocks.context.Context[In]): Resource[Out]
Wires:
object Wire:
final case class Shared[-In, +Out](makeFn: (Scope, Context[In]) => Out) extends Wire[In, Out]
final case class Unique[-In, +Out](makeFn: (Scope, Context[In]) => Out) extends Wire[In, Out]
def apply[T](t: T): Wire.Shared[Any, T]
transparent inline def shared[T]: Wire.Shared[?, T]
transparent inline def unique[T]: Wire.Unique[?, T]
Notes:
Wire(t)wraps a pre-existing value; if it'sAutoCloseable,close()is registered automatically when used.
Unscoped[A]
trait Unscoped[A]
object Unscoped:
inline given derived[A](using scala.deriving.Mirror.Of[A]): Unscoped[A]
// plus many built-in givens (primitives, collections, time, UUID, Chunk, ...)
Practical guidance (summary)
- Allocate in a scope:
resource.allocate(insideScope.global.scoped { scope => import scope.* ... }) - Access one scoped value:
$(sa)(v => v.method())— parameter can only be a receiver - Access two or more scoped values simultaneously:
$(sa1, sa2)((v1, v2) => v1.method(v2.result()))(N=2..5) - For N>5: call
$once per resource, combine the plain results - Return only
Unscopeddata fromscopedblocks - Use
lowerto use parent values inside a child - If
$returns$[Resource[A]], call.allocateon it (scoped resource chaining) - Use
open()for explicitly-managed, cross-thread capable scopes - Use
leakonly when interop forces it; preferUnscopedfor pure data