skip to content

When do you use .run() versus .call() on a ScopedValue binding, and what are common usage pitfalls?

level: middleimportance: should knowfreq 25%

answer

  1. run = void + unchecked; call = returns value / checked throw
  2. get() outside scope -> NoSuchElementException
  3. binding dies when run/call returns
  4. escaped callbacks/bare threads read it after scope closed = fail
  5. no set(); mutable bound object can still be mutated

basics

~20 s

Use .run() when the task returns nothing and throws only unchecked exceptions; use .call() when the task returns a value or throws a checked exception. Common mistakes: reading the value outside the scope (throws NoSuchElementException) and expecting the binding to outlive the run/call call.

solid answer

~50 s

After ScopedValue.where(KEY, value) you terminate with either .run() or .call(). Use .run(Runnable) for a side-effecting task that returns nothing and throws only unchecked exceptions. Use .call(CallableOp) when the task produces a result or may throw a checked exception — .call returns that result and propagates the checked exception. Both bind the value for the task's dynamic extent and tear it down on return. Common pitfalls: (1) calling KEY.get() outside any binding throws NoSuchElementException — guard with isBound() or orElse(); (2) expecting the binding to persist after run/call returns — it doesn't; (3) capturing the value in a callback or a bare thread that escapes the scope, then reading it after the scope closed — unbound by then; (4) trying to mutate via a non-existent set(); (5) binding a mutable object and mutating its contents, reintroducing the action-at-a-distance you wanted to avoid. The right pattern is: bind, do all the work that needs it inside run/call, and treat the value as a scope-confined constant.

code

java · 13 lines
java
static final ScopedValue<User> USER = ScopedValue.newInstance();

// .run(): void task, unchecked exceptions only
ScopedValue.where(USER, alice).run(() -> auditLogin());

// .call(): returns a value and may throw a checked exception
try {
    Report r = ScopedValue.where(USER, alice).call(() -> buildReport()); // throws IOException
} catch (IOException e) { /* handle */ }

// Safe reads:
String name = USER.isBound() ? USER.get().name() : "anonymous";
// USER.get() here, outside any where(...).run/call, would throw NoSuchElementException.

go deeper

for a junior

Knows .run() is for void tasks and .call() is for tasks that return a value, and that get() only works inside the scope.

for a middle

Distinguishes run vs call by return value and checked-exception handling, and lists the main pitfalls (escaping the scope, NoSuchElementException, no set()).

for a senior

Explains why escaping closures fail (value tied to dynamic extent), the structured-concurrency remedy, and the mutable-object subtlety.

for a principal

Establishes team conventions for binding at boundaries, avoiding scope escape, immutable values only, and safe migration patterns from ad-hoc context passing.

## The two terminators A binding expression is `ScopedValue.where(KEY, value)` — but that on its own does nothing; you must run a task within it. There are two ways: - **`.run(Runnable op)`** — for a task that returns **no value** and throws only **unchecked** exceptions (RuntimeExceptions/Errors). It returns `void`. - **`.call(CallableOp<T, X> op)`** — for a task that **returns a value** of type `T` and/or may throw a **checked** exception `X`. `.call` returns the task's result and propagates its checked exception, so the caller must handle/declare it. Why two? Java's lambda typing distinguishes a `Runnable` (no return, no checked exceptions) from a callable-style functional interface (returns a value, may throw checked). Picking the right one is just matching your task's shape: *need a result or a checked throw → `.call`; pure side effect → `.run`.* ```java // run: side effect, void, unchecked only ScopedValue.where(USER, alice).run(() -> auditLogin()); // call: returns a value and may throw a checked exception Report r = ScopedValue.where(USER, alice).call(() -> buildReport()); // throws IOException ``` You can chain multiple bindings before the terminator: `ScopedValue.where(A, x).where(B, y).call(() -> ...)`. ## Pitfall 1 — reading outside the scope `KEY.get()` only works while inside the binding. Outside it (before binding, after it returns, or on an unrelated thread) it throws **`NoSuchElementException`**. Defensive reads: - `KEY.isBound()` → boolean check first. - `KEY.orElse(fallback)` → value or a default. - `KEY.orElseThrow(supplier)` → value or your own exception. ## Pitfall 2 — expecting persistence The binding lives **exactly** for the `run`/`call` invocation. After it returns (normally or by exception), the value is unbound again. People sometimes bind a value, return from the method, and later expect `get()` to still work — it won't. ## Pitfall 3 — escaping the scope If inside the scope you start a *bare* thread, schedule an async callback, or store a `() -> KEY.get()` lambda that runs *later*, and that work executes **after** the scope has closed, the read fails (unbound). The value is tied to the *dynamic extent*, not captured by closures that outlive it. The correct approach is to fork subtasks via **structured concurrency** so they run *within* the scope (then they inherit the binding), or to read the value *inside* the scope and pass the plain value forward. ## Pitfall 4 — looking for set() There is no `set()`/`update()`. To use a different value in a sub-region, open a nested `where(...)` (covered in the immutability question). Searching for a mutator is the most common newcomer mistake. ## Pitfall 5 — binding mutable state The *binding* (which object is bound) is immutable, but if you bind a mutable object (a list, a map, a builder) callees can still mutate its **contents**. That quietly brings back the action-at-a-distance you adopted ScopedValue to avoid. Prefer binding immutable values; if you must share mutable state, do it deliberately and document it. ## The clean usage pattern ```text 1. Declare: static final ScopedValue<T> KEY = ScopedValue.newInstance(); 2. Bind at the boundary where context is known (request entry, etc.). 3. Do ALL work needing the value inside run()/call(). 4. Read via get() (or isBound/orElse) only within that extent. 5. Don't let the value escape; if you fork, use structured concurrency. ``` Follow that and the value behaves as a safe, self-cleaning, scope-confined constant.

  • You bind a value, then submit a task to an executor that runs later; the task calls KEY.get() and fails. Why?
    The executor task runs after your binding scope has returned, so KEY is unbound by then and get() throws NoSuchElementException. Either read the value inside the scope and pass the plain value to the task, or fork within a structured-concurrency scope so the subtask runs inside the binding's extent and inherits it.
  • Your task throws a checked exception but you used .run() and it won't compile — what's the fix?
    Switch to .call(), which accepts a task that may throw a checked exception (and can also return a value). .run() only accepts a Runnable, which permits unchecked exceptions only.

saying these in an interview costs you the question

  • Using .run() for a task that returns a value or throws a checked exception
  • Reading get() outside the binding without isBound()/orElse and expecting null instead of an exception
  • Capturing the value in a lambda/thread that escapes and runs after the scope closes
  • Assuming binding a mutable object keeps its contents immutable

context