Scala 3 given and using: making contextual abstractions explicit
Scala 2's implicit keyword hid dependencies. Scala 3's given and using split declaration from request, improving readability, scoping, and compiler diagnostics. Learn how to migrate safely.
27 Aug 2025, 13:00 UTC

The problem with silent injection
Scala 2's implicit keyword did two jobs at once: it marked values that could be injected and parameters that expected injection. That overloading made call sites clean but hid the dependency graph. When an implicit was missing, the compiler error often appeared far from the root cause. Scala 3 splits the concept into given and using. The takeaway is that you gain explicit, scoped control over contextual instances without losing the ergonomic benefit of automatic injection.
Two keywords, one job
A given instance is a value the compiler can supply automatically. A using parameter is a request for such a value. The distinction matters because the declaration site and the consumption site now use different vocabulary, which reduces the cognitive load when reading unfamiliar code.
Consider a simple type class—a trait that adds behavior to a type without inheritance:
trait Show[A]:
def show(a: A): String
case class User(name: String, age: Int)
object User:
given Show[User] with
def show(u: User) = s"User(${u.name}, ${u.age})"
def render[A](a: A)(using s: Show[A]): String =
s.show(a)
Here render asks for a Show[A] through using. The compiler looks for a matching given in scope. Because the instance lives in the User companion object, it is found automatically when User is involved, following the same companion-object lookup rules that Scala developers already know.
Scope is the new safety boundary
One of the most practical changes is that given visibility follows normal lexical scoping. A given defined inside a method, block, or class is only visible there. That makes dependency injection explicit and testable.
For example, if you move the given into a local block:
def render[A](a: A)(using s: Show[A]): String =
s.show(a)
@main def demo(): Unit =
given Show[User] with
def show(u: User) = "local"
println(render(User("Ada", 36)))
This compiles because the given is in the same scope as the call. If you delete the local given and do not import one, the compiler rejects the call to render and points to the using parameter that cannot be satisfied. That diagnostic is narrower than the old implicit-not-found errors, which often listed every implicit in the universe.
Derivation and diagnostics
Scala 3 also adds derived methods for automatic instance generation. For case classes, you can ask the compiler to build a given for a product type by combining the instances of its fields. This reduces boilerplate when you need, say, encoders for nested data structures.
The compiler diagnostics improve alongside this. When a using parameter is missing, the error message identifies the exact parameter list position. When two given instances of the same type are visible in the same scope, the compiler reports an ambiguity rather than silently picking one. That turns a class of runtime surprises into compile-time failures.
Trade-offs: migration and accidental breadth
The main limitation is transition friction. Scala 3 still accepts the old implicit syntax, so a codebase can mix both styles. That creates a real hazard: an implicit value in an outer scope can shadow a given in an inner scope, or vice versa, depending on import order and companion-object priority. The safest path is to migrate module by module, converting implicit declarations to given and implicit parameters to using together, then compiling before moving to the next module.
A second limitation is breadth. A given placed in a package object is visible everywhere that package is imported. If it is too general—for example, a Show[Any]—it can be selected unintentionally for unrelated types. Keep given instances as narrow as possible and prefer companion objects over package-level declarations.
How to verify the behavior in your project
You do not need a complex build to confirm these mechanics. In an sbt project (run commands from the project root with normal user permissions), create a file with the Show trait and the render method above. Then:
- Run
sbt compile. Expect success when a matchinggivenis in scope. - Remove or comment out the
givenand runsbt compileagain. Expect a failure that references theusingparameter. - Add a second
givenof the same type in the same scope and compile. Expect an ambiguity error.
These checks take seconds and confirm that scoping and diagnostics behave as described on your Scala 3 version. If you are maintaining a mixed codebase, run the same compile after each migration step to catch shadowing early.
Start by converting one leaf module: replace implicit with given at declaration sites and using at parameter sites, compile, and inspect the error messages. The explicitness is the feature—use it to make your contextual dependencies visible, not just automatic.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.