Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Scoped values

Some data is needed by many methods at once: the current user, a request number, the interface language. Passing it as a parameter through five levels of calls is tedious, and a global variable won't do — in a multithreaded program each thread has its own user. Java 25 has scoped values for this: a value is bound for the time some code runs, and any method inside that code can read it.

The first value

A scoped value is created once, usually in a static final field, with ScopedValue.newInstance(). Then ScopedValue.where(key, value) binds a value, and run(...) executes code inside which it's available through get():

static final ScopedValue<String> USER = ScopedValue.newInstance();
 
void main() {
    ScopedValue.where(USER, "Anna").run(() -> showProfile());
}
 
void showProfile() {
    IO.println("Profile: " + USER.get());
}

Output:

Profile: Anna

The showProfile method doesn't receive the user as a parameter — it takes it from USER. The USER field itself isn't the value but a key used to find the value.

Only inside run

The binding lasts exactly as long as run executes — hence the name "scoped". After run returns, there's no value again. You can check the binding with isBound(), and orElse(...) returns a fallback value. If you need a result, call call instead of run:

static final ScopedValue<String> USER = ScopedValue.newInstance();
 
void main() throws Exception {
    String greeting = ScopedValue.where(USER, "Oleg").call(() -> "Hello, " + USER.get());
    IO.println(greeting);
 
    IO.println("Bound: " + USER.isBound());
    IO.println("User: " + USER.orElse("guest"));
}

Output:

Hello, Oleg
Bound: false
User: guest

You can't change the value inside run: ScopedValue has no set method. Data flows only downwards, and any code sees what its caller bound.

Nested binding

Inside run you can bind another value to the same key — for example, an administrator acting on behalf of a user. The new value is visible only in the nested run, and after it the previous one comes back:

static final ScopedValue<String> USER = ScopedValue.newInstance();
 
void main() {
    ScopedValue.where(USER, "admin").run(() -> {
        log("start of work");
        ScopedValue.where(USER, "Anna").run(() -> log("password change"));
        log("end of work");
    });
}
 
void log(String action) {
    IO.println("[" + USER.get() + "] " + action);
}

Output:

[admin] start of work
[Anna] password change
[admin] end of work

Instead of ThreadLocal

This used to be solved with the ThreadLocal class — a variable that has its own value in each thread. It has drawbacks: the value can be changed from anywhere, and you must remember to delete it with remove(), otherwise it "leaks" to the next task in a pool. And with a million virtual threads, a million copies take a lot of memory. A scoped value is immutable, disappears by itself and is cheap. Each task binds its own value:

static final ScopedValue<String> REQUEST_ID = ScopedValue.newInstance();
 
String handle(String id) {
    return ScopedValue.where(REQUEST_ID, id).call(() -> loadOrder());
}
 
String loadOrder() {
    return "request " + REQUEST_ID.get() + ": order loaded";
}
 
void main() throws Exception {
    try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
        List<Future<String>> results = new ArrayList<>();
        for (String id : List.of("A-1", "B-2", "C-3")) {
            results.add(executor.submit(() -> handle(id)));
        }
        for (Future<String> result : results) {
            IO.println(result.get());
        }
    }
}

Output:

request A-1: order loaded
request B-2: order loaded
request C-3: order loaded

The tasks run at the same time, but each sees only its own request number. Threads created through an ExecutorService don't inherit the parent's binding, so where is called inside the task. Values are passed to child tasks automatically by structured concurrency (StructuredTaskScope), but in Java 25 it's still a preview feature.

Common mistake

Calling get() outside run — for example, when a method is called directly rather than from inside a binding — throws an exception:

static final ScopedValue<String> USER = ScopedValue.newInstance();
 
void main() {
    showProfile();
}
 
void showProfile() {
    IO.println("Profile: " + USER.get());
}

Output:

Exception in thread "main" java.util.NoSuchElementException: ScopedValue not bound
	at java.base/java.lang.ScopedValue.slowGet(ScopedValue.java:571)
	at java.base/java.lang.ScopedValue.get(ScopedValue.java:564)
	at Main.showProfile(Main.java:8)
	at Main.main(Main.java:4)

If the method can also work without a binding, use USER.orElse("guest") or check USER.isBound().

Thanks for reading!

Donate to the site — we work hard for you.