Lazylock

188. LazyLock::from — Skip the Closure When You Already Have the Value

Sometimes your “lazy” value isn’t lazy at all — a test or a CLI flag hands it to you up front. Rust 1.96 stabilized From<T> for LazyLock<T>, so you can build an already-initialized lock straight from the value.

The old workaround was to wrap the known value in a move closure anyway:

1
2
// Pretend-lazy: the value sits captive until the first deref
let lock = LazyLock::new(move || url);

It compiles, but the lock reports “not initialized” until someone derefs it — even though you had the value the whole time. As of Rust 1.96, LazyLock::from (or .into()) builds the lock pre-initialized:

1
2
3
4
5
6
use std::sync::LazyLock;

let eager: LazyLock<u32> = LazyLock::from(42);

// Initialized immediately — no deref needed first
assert_eq!(LazyLock::get(&eager), Some(&42));

The practical win is mixing eager and lazy at runtime. From produces the default F = fn() -> T parameter — the same type a non-capturing closure coerces to — so both branches unify:

1
2
3
4
5
6
7
8
fn api_url(cli_override: Option<String>) -> LazyLock<String> {
    match cli_override {
        // Value already known: initialized, closure never exists
        Some(url) => LazyLock::from(url),
        // Computed on first use, as usual
        None => LazyLock::new(|| std::env::var("API_URL").unwrap()),
    }
}

Before 1.96 the Some arm needed LazyLock::new(move || url) — deferring initialization for no reason and making LazyLock::get lie to you in tests.

The single-threaded sibling From<T> for LazyCell<T> landed in the same release, so the trick works in both std::sync and std::cell flavors.

#158 May 2026

158. OnceLock<T> and LazyLock<T, F> — The std Replacements for lazy_static!

This morning’s Atomic* handled “many threads, one scalar.” For “many threads, one value — computed once, then read forever” the std answer is two types: LazyLock<T, F> when you can name the initializer up front, OnceLock<T> when you only learn the value at runtime. Both make the old lazy_static! macro and the once_cell crate redundant.

The bad old days: lazy_static!

Until Rust 1.80 (mid-2024), a thread-safe lazy global meant a macro from a crate:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// Cargo.toml: lazy_static = "1"
use lazy_static::lazy_static;
use std::collections::HashMap;

lazy_static! {
    static ref COUNTRIES: HashMap<&'static str, &'static str> = {
        let mut m = HashMap::new();
        m.insert("fr", "France");
        m.insert("de", "Germany");
        m
    };
}

fn main() {
    assert_eq!(COUNTRIES.get("fr"), Some(&"France"));
}

It worked, but it pulled in a dependency, used macro magic to fake a static, and gave you a custom Deref wrapper instead of a normal type. Every dependency in your tree that wanted a lazy global was either pulling in lazy_static or the more modern once_cell crate.

The std way: LazyLock<T, F>

Stabilized in Rust 1.80, LazyLock is a normal type — no macro, no Deref trickery, just a static like any other:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
use std::collections::HashMap;
use std::sync::LazyLock;

static COUNTRIES: LazyLock<HashMap<&'static str, &'static str>> = LazyLock::new(|| {
    let mut m = HashMap::new();
    m.insert("fr", "France");
    m.insert("de", "Germany");
    m
});

assert_eq!(COUNTRIES.get("fr"), Some(&"France"));

The closure runs the first time anything touches COUNTRIES, exactly once, and every thread that races to be that “first” gets the same value back. If two threads arrive together, one wins the init and the other blocks until it’s done — same contract lazy_static! always had, now in std.

When the initializer isn’t known at compile time: OnceLock<T>

LazyLock is great when you can write the initializer as a const fn-friendly closure. But what about config you only know after main() starts — a CLI flag, an env var, a parsed file? Stuffing that into a closure means either re-reading the env var at first use or capturing values you don’t have yet at static time. That’s OnceLock<T>’s job:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
use std::sync::OnceLock;

static GREETING: OnceLock<String> = OnceLock::new();

fn init_from_args(name: &str) {
    GREETING.set(format!("hello, {name}")).ok();   // ok() = ignore "already set"
}

fn greeting() -> &'static str {
    GREETING.get().map(String::as_str).unwrap_or("hello, world")
}

init_from_args("ferris");
assert_eq!(greeting(), "hello, ferris");

set returns Err(value) if someone beat you to it — handy when multiple call sites might initialize and you want the first one to win without panicking.

The convenience method: get_or_init

The “check if set, init if not” dance is so common that OnceLock ships it as one call:

1
2
3
4
5
6
7
8
9
use std::sync::OnceLock;

fn config() -> &'static String {
    static CFG: OnceLock<String> = OnceLock::new();
    CFG.get_or_init(|| std::env::var("APP_CFG").unwrap_or_else(|_| "default".into()))
}

assert!(!config().is_empty());
assert_eq!(config(), config());    // same &'static str on every call

This is what most “lazy global computed from runtime data” code actually wants. Functions can own their own OnceLock as a function-local static — no need to pollute the module namespace.

Which one when

You haveReach for
A closure that needs no runtime inputLazyLock<T, F>
A value you’ll set later (from main, a builder, a DI container)OnceLock<T> with set
Per-function memoization of an expensive computationOnceLock<T> with get_or_init
Single-threaded version of the aboveLazyCell / OnceCell

LazyLock is the closer match to lazy_static!. OnceLock is the closer match to manual Mutex<Option<T>> patterns where you wanted “set once, read many.”

The trait bounds, briefly

Because these are Sync and live in a static, the contained T must be Send + Sync. The initializer closure for LazyLock must be Send. You won’t notice for String, HashMap, Vec, Arc<…> — but if you try to stuff an Rc<T> in there the compiler will (correctly) yell. The single-threaded versions, LazyCell and OnceCell, have no such bound — that’s the whole reason both pairs exist.

What you can finally delete

If a crate in your tree still has lazy_static = "1" or once_cell = "1" in its Cargo.toml, and your MSRV is 1.80 or newer, the migration is mechanical:

1
2
3
4
// before
lazy_static! { static ref X: T = init(); }
// after
static X: LazyLock<T> = LazyLock::new(init);
1
2
3
4
5
6
// before
use once_cell::sync::OnceCell;
static X: OnceCell<T> = OnceCell::new();
// after
use std::sync::OnceLock;
static X: OnceLock<T> = OnceLock::new();

One fewer dependency, one less macro in the expansion, and the type that shows up in error messages is just LazyLock<T> — not some crate-private deref wrapper. Tomorrow’s bite picks up the thread on Arc<T> — what to reach for when “one global value” isn’t enough and you need shared ownership across threads.

95. LazyLock::get — Peek at a Lazy Value Without Initializing It

Wanted to know whether a LazyLock has been initialized yet — without causing the initialization by touching it? Rust 1.94 stabilises LazyLock::get and LazyCell::get, which return Option<&T> and leave the closure untouched if it hasn’t fired.

The old pain

Any access that goes through Deref forces the closure to run. So the moment you do *CONFIG — or anything that implicitly derefs — the lazy becomes eager. Before 1.94 there was no way to ask “has this been initialized?” without tripping that wire.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
use std::sync::LazyLock;

static CONFIG: LazyLock<Vec<String>> = LazyLock::new(|| {
    // Imagine this is slow: reads a file, hits the network, etc.
    vec!["debug".into(), "verbose".into()]
});

fn main() {
    // No way to peek without paying the init cost
    let _ = CONFIG.len(); // forces init
    assert_eq!(CONFIG.len(), 2);
}

Handy enough, but if you want a metric like “was the config ever read?” you’d have to wrap the whole thing in your own AtomicBool.

The fix: LazyLock::get

Called as an associated function (LazyLock::get(&lock)), it returns Option<&T> without touching the closure. None means the lazy is still pending. Some(&value) means someone already forced it.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
use std::sync::LazyLock;

static CONFIG: LazyLock<Vec<String>> = LazyLock::new(|| {
    vec!["debug".into(), "verbose".into()]
});

fn main() {
    // Not yet initialised — get returns None
    assert!(LazyLock::get(&CONFIG).is_none());

    // Force init via normal deref
    assert_eq!(CONFIG.len(), 2);

    // Now get returns Some(&value)
    let peek = LazyLock::get(&CONFIG).unwrap();
    assert_eq!(peek.len(), 2);
}

Note the call style: LazyLock::get(&CONFIG), not CONFIG.get(). That’s deliberate — method-lookup on the lock itself would go through Deref, which is exactly the thing we’re trying to avoid.

Same story for LazyCell

LazyCell is the single-threaded cousin and gets the same treatment:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
use std::cell::LazyCell;

fn main() {
    let greeting = LazyCell::new(|| "Hello, Rust!".to_uppercase());

    // Not forced yet
    assert!(LazyCell::get(&greeting).is_none());

    // Deref triggers the closure
    assert_eq!(*greeting, "HELLO, RUST!");

    // Now we can peek without re-running anything
    assert_eq!(LazyCell::get(&greeting), Some(&"HELLO, RUST!".to_string()));
}

Where this shines

Two patterns fall out naturally.

Metrics and diagnostics. You want to log “did we ever load the config?” at shutdown without accidentally loading it just to find out:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
use std::sync::LazyLock;

static CACHE: LazyLock<Vec<u64>> = LazyLock::new(|| (0..1000).collect());

fn was_cache_used() -> bool {
    LazyLock::get(&CACHE).is_some()
}

fn main() {
    assert!(!was_cache_used());
    let _ = CACHE.first(); // touch it
    assert!(was_cache_used());
}

Tests. Assert that the lazy init didn’t happen on a code path that shouldn’t need it — a guarantee that was basically impossible to write before.

When to reach for it

Use LazyLock::get / LazyCell::get whenever you need to ask about initialization state without causing it. For everything else, just deref as usual — that’s still the one-liner that Just Works.

Stabilised in Rust 1.94 (March 2026).

35. LazyLock

Still pulling in lazy_static or once_cell just for a lazy global? std::sync::LazyLock does the same thing — zero dependencies.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
use std::sync::LazyLock;

static CONFIG: LazyLock<Vec<String>> = LazyLock::new(|| {
    // Imagine this reads from a file or env
    vec!["debug".to_string(), "verbose".to_string()]
});

fn main() {
    // CONFIG is initialized on first access
    println!("flags: {:?}", *CONFIG);
    assert_eq!(CONFIG.len(), 2);
}

LazyLock was stabilized in Rust 1.80 as the std replacement for once_cell::sync::Lazy and lazy_static!. It initializes the value exactly once on first access, is Sync by default, and works in static items without macros.

For single-threaded or non-static use, there’s also LazyCell — same idea but without the synchronization overhead:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
use std::cell::LazyCell;

fn main() {
    let greeting = LazyCell::new(|| {
        println!("computing...");
        "Hello, Rust!".to_uppercase()
    });

    println!("before access");
    // "computing..." prints here, on first deref
    assert_eq!(*greeting, "HELLO, RUST!");
    // second access — no recomputation
    assert_eq!(*greeting, "HELLO, RUST!");
}

The output is:

1
2
before access
computing...

The closure runs lazily on first Deref, and the result is cached for all subsequent accesses. No unwrap(), no Mutex, no external crates — just clean lazy initialization from std.