How do I work with lazy static initialization with once_cell in Rust?

Walkthrough

The once_cell crate provides types for single-assignment cells and lazy static initialization. Unlike lazy_static!, once_cell provides a more ergonomic API using types like Lazy<T> and OnceCell<T>. These types ensure a value is initialized exactly once and can be accessed efficiently afterward. Once_cell is ideal for global static variables, configuration caching, thread-safe lazy initialization, and any scenario where you need to defer expensive initialization until first use.

Key concepts:

  1. Lazy — value is initialized on first access
  2. OnceCell — single-assignment cell for optional initialization
  3. sync vs unsync — thread-safe (sync) and single-threaded (unsync) variants
  4. No Macros — uses types instead of macros for cleaner code
  5. Composable — works well with other patterns and types

Code Example

# Cargo.toml
[dependencies]
once_cell = "1.0"
use once_cell::sync::Lazy;
 
static CONFIG: Lazy<Vec<String>> = Lazy::new(|| {
    vec![
        "setting1".to_string(),
        "setting2".to_string(),
    ]
});
 
fn main() {
    println!("Config: {:?}", *CONFIG);
}

Basic Lazy Static

use once_cell::sync::Lazy;
 
// Global static with lazy initialization
static NUMBERS: Lazy<Vec<i32>> = Lazy::new(|| {
    println!("Initializing NUMBERS...");
    (1..=5).collect()
});
 
fn main() {
    println!("Before first access");
    
    // First access triggers initialization
    println!("First: {:?}", *NUMBERS);
    
    // Subsequent accesses reuse the cached value
    println!("Second: {:?}", *NUMBERS);
    println!("Third: {:?}", *NUMBERS);
    
    // "Initializing NUMBERS..." only prints once
}

Compare with lazy_static!

use once_cell::sync::Lazy;
 
// once_cell style (recommended)
static NAME: Lazy<String> = Lazy::new(|| "Alice".to_string());
 
// lazy_static! style (older approach)
// lazy_static! {
//     static ref NAME: String = "Alice".to_string();
// }
 
fn main() {
    // once_cell allows dereferencing naturally
    println!("Name: {}", *NAME);
    println!("Length: {}", NAME.len());
    
    // Can use in type positions easily
    let names: [&Lazy<String>; 1] = [&NAME];
    for n in names {
        println!("Name: {}", **n);
    }
}

Thread-Safe Lazy with Mutex

use once_cell::sync::Lazy;
use std::sync::Mutex;
 
static COUNTER: Lazy<Mutex<i32>> = Lazy::new(|| Mutex::new(0));
 
fn main() {
    // Access and modify the counter
    {
        let mut num = COUNTER.lock().unwrap();
        *num += 1;
        println!("Counter: {}", *num);
    }
    
    {
        let mut num = COUNTER.lock().unwrap();
        *num += 1;
        println!("Counter: {}", *num);
    }
}

OnceCell for Optional Initialization

use once_cell::sync::OnceCell;
 
static INSTANCE: OnceCell<String> = OnceCell::new();
 
fn main() {
    // Check if initialized
    println!("Is initialized: {}", INSTANCE.get().is_some());
    
    // Initialize once
    INSTANCE.set("Hello".to_string()).ok();
    
    // Already initialized, set returns Err
    let result = INSTANCE.set("World".to_string());
    println!("Second set result: {:?}", result); // Err("World")
    
    // Get the value
    println!("Value: {:?}", INSTANCE.get());
    
    // Get or initialize
    let value = INSTANCE.get_or_init(|| {
        println!("This won't run - already initialized");
        "New Value".to_string()
    });
    println!("Value: {}", value);
}

OnceCell in Structs

use once_cell::unsync::OnceCell;
 
struct Config {
    name: String,
    // Expensive to compute, computed lazily
    derived: OnceCell<String>,
}
 
impl Config {
    fn new(name: &str) -> Self {
        Self {
            name: name.to_string(),
            derived: OnceCell::new(),
        }
    }
    
    fn get_derived(&self) -> &str {
        self.derived.get_or_init(|| {
            println!("Computing derived value...");
            format!("derived_{}", self.name)
        })
    }
}
 
fn main() {
    let config = Config::new("test");
    
    println!("Before derived");
    println!("Derived: {}", config.get_derived());
    println!("Derived: {}", config.get_derived()); // Uses cached value
}

Configuration Singleton

use once_cell::sync::Lazy;
use std::collections::HashMap;
 
#[derive(Debug)]
struct AppConfig {
    database_url: String,
    api_key: String,
    max_connections: usize,
}
 
impl AppConfig {
    fn from_env() -> Self {
        // Simulate reading from environment
        println!("Loading configuration...");
        Self {
            database_url: "postgresql://localhost/db".to_string(),
            api_key: "secret-key".to_string(),
            max_connections: 10,
        }
    }
}
 
static CONFIG: Lazy<AppConfig> = Lazy::new(AppConfig::from_env);
 
fn get_database_url() -> &'static str {
    &CONFIG.database_url
}
 
fn get_api_key() -> &'static str {
    &CONFIG.api_key
}
 
fn main() {
    println!("App starting...");
    
    // Config is loaded on first access
    println!("DB: {}", get_database_url());
    println!("API Key: {}", get_api_key());
    
    // Subsequent calls use the cached config
    println!("Max connections: {}", CONFIG.max_connections);
}

Regex Cache

use once_cell::sync::Lazy;
use regex::Regex;
use std::collections::HashMap;
 
static REGEX_CACHE: Lazy<Mutex<HashMap<&'static str, Regex>>> = 
    Lazy::new(|| Mutex::new(HashMap::new()));
 
use std::sync::Mutex;
 
fn get_regex(pattern: &'static str) -> Result<Regex, regex::Error> {
    {
        let cache = REGEX_CACHE.lock().unwrap();
        if let Some(re) = cache.get(pattern) {
            return Ok(re.clone());
        }
    }
    
    let re = Regex::new(pattern)?;
    let mut cache = REGEX_CACHE.lock().unwrap();
    cache.insert(pattern, re.clone());
    Ok(re)
}
 
fn main() {
    let email_re = get_regex(r"^[^@]+@[^@]+\.[^@]+$").unwrap();
    
    println!("Is email: {}", email_re.is_match("test@example.com"));
    println!("Is email: {}", email_re.is_match("invalid"));
}

HTTP Client Singleton

use once_cell::sync::Lazy;
use std::time::Duration;
 
// Simulated HTTP client
#[derive(Debug)]
struct HttpClient {
    timeout: Duration,
    base_url: String,
}
 
impl HttpClient {
    fn new() -> Self {
        println!("Creating HTTP client...");
        Self {
            timeout: Duration::from_secs(30),
            base_url: "https://api.example.com".to_string(),
        }
    }
    
    fn get(&self, path: &str) -> String {
        format!("GET {}{}", self.base_url, path)
    }
}
 
static HTTP_CLIENT: Lazy<HttpClient> = Lazy::new(HttpClient::new);
 
fn make_request(path: &str) -> String {
    HTTP_CLIENT.get(path)
}
 
fn main() {
    println!("Making requests...");
    println!("{}", make_request("/users"));
    println!("{}", make_request("/posts"));
}

Thread-Local Lazy

use once_cell::unsync::Lazy;
use std::cell::RefCell;
 
thread_local! {
    static THREAD_STATE: Lazy<RefCell<Vec<String>>> = Lazy::new(|| {
        println!("Initializing thread state for thread {:?}", std::thread::current().id());
        RefCell::new(Vec::new())
    });
}
 
fn add_item(item: &str) {
    THREAD_STATE.with(|state| {
        state.borrow_mut().push(item.to_string());
    });
}
 
fn get_items() -> Vec<String> {
    THREAD_STATE.with(|state| state.borrow().clone())
}
 
fn main() {
    add_item("first");
    add_item("second");
    println!("Items: {:?}", get_items());
}

Lazy with Dependencies

use once_cell::sync::Lazy;
 
// Dependencies between lazy statics
static BASE_URL: Lazy<String> = Lazy::new(|| {
    println!("Initializing BASE_URL");
    "https://api.example.com".to_string()
});
 
static API_ENDPOINT: Lazy<String> = Lazy::new(|| {
    println!("Initializing API_ENDPOINT");
    format!("{}/v1/users", *BASE_URL)
});
 
static FULL_CONFIG: Lazy<String> = Lazy::new(|| {
    println!("Initializing FULL_CONFIG");
    format!("Endpoint: {}", *API_ENDPOINT)
});
 
fn main() {
    // Access order determines initialization order
    println!("Full config: {}", *FULL_CONFIG);
    // This triggers: BASE_URL -> API_ENDPOINT -> FULL_CONFIG
}

Get or Init Patterns

use once_cell::sync::OnceCell;
 
static CACHE: OnceCell<Vec<String>> = OnceCell::new();
 
fn get_or_compute() -> &'static Vec<String> {
    CACHE.get_or_init(|| {
        println!("Computing cache...");
        (1..=5).map(|i| format!("item_{}", i)).collect()
    })
}
 
fn try_initialize(value: Vec<String>) -> bool {
    CACHE.set(value).is_ok()
}
 
fn main() {
    // Try to set (succeeds if not already set)
    try_initialize(vec!["preloaded".to_string()]);
    
    // Get or compute
    let items = get_or_compute();
    println!("Items: {:?}", items);
    
    // Get existing
    if let Some(existing) = CACHE.get() {
        println!("Already cached: {:?}", existing);
    }
}

unsync OnceCell for Single-Threaded

use once_cell::unsync::OnceCell;
 
fn main() {
    // unsync is faster when thread safety isn't needed
    let cell = OnceCell::new();
    
    // Set initial value
    cell.set(42).unwrap();
    
    // Get the value
    println!("Value: {:?}", cell.get());
    
    // Trying to set again fails
    assert!(cell.set(100).is_err());
    
    // get_mut for mutable access (only available on unsync)
    if let Some(value) = cell.get_mut() {
        *value *= 2;
    }
    
    println!("Modified: {:?}", cell.get());
    
    // take() removes and returns the value
    let taken = cell.take();
    println!("Taken: {:?}", taken);
    println!("After take: {:?}", cell.get());
}

Lazy with Initialization Function

use once_cell::sync::Lazy;
 
enum Environment {
    Development,
    Production,
}
 
static ENV: Lazy<Environment> = Lazy::new(|| {
    // Simulate environment detection
    std::env::var("ENV").map(|_| Environment::Production).unwrap_or(Environment::Development)
});
 
static DEBUG_MODE: Lazy<bool> = Lazy::new(|| {
    matches!(*ENV, Environment::Development)
});
 
fn main() {
    println!("Debug mode: {}", *DEBUG_MODE);
}

Database Connection Pool

use once_cell::sync::Lazy;
use std::sync::Arc;
 
// Simulated connection pool
#[derive(Debug)]
struct ConnectionPool {
    url: String,
    max_connections: usize,
}
 
impl ConnectionPool {
    fn new(url: &str, max: usize) -> Self {
        println!("Creating connection pool to {}", url);
        Self {
            url: url.to_string(),
            max_connections: max,
        }
    }
    
    fn get_connection(&self) -> String {
        format!("Connection to {}", self.url)
    }
}
 
static POOL: Lazy<Arc<ConnectionPool>> = Lazy::new(|| {
    Arc::new(ConnectionPool::new("postgresql://localhost/mydb", 10))
});
 
fn query(sql: &str) -> String {
    let conn = POOL.get_connection();
    format!("Executing '{}' on {}", sql, conn)
}
 
fn main() {
    println!("{}", query("SELECT * FROM users"));
    println!("{}", query("SELECT * FROM posts"));
}

OnceCell with Error Handling

use once_cell::sync::OnceCell;
use std::sync::Mutex;
 
static PARSED_CONFIG: OnceCell<Result<Config, String>> = OnceCell::new();
 
#[derive(Debug, Clone)]
struct Config {
    value: i32,
}
 
fn parse_config() -> Result<Config, String> {
    // Simulate parsing
    Ok(Config { value: 42 })
}
 
fn get_config() -> Result<&'static Config, &'static str> {
    let result = PARSED_CONFIG.get_or_init(|| {
        parse_config()
    });
    
    result.as_ref().map_err(|e| e.as_str())
}
 
fn main() {
    match get_config() {
        Ok(config) => println!("Config value: {}", config.value),
        Err(e) => println!("Error: {}", e),
    }
}

Lazy Collections

use once_cell::sync::Lazy;
use std::collections::HashMap;
 
static WORD_FREQUENCIES: Lazy<HashMap<&'static str, usize>> = Lazy::new(|| {
    let text = "the quick brown fox jumps over the lazy dog the fox";
    let mut freq = HashMap::new();
    
    for word in text.split_whitespace() {
        *freq.entry(word).or_insert(0) += 1;
    }
    
    freq
});
 
fn word_count(word: &str) -> Option<usize> {
    WORD_FREQUENCIES.get(word).copied()
}
 
fn main() {
    println!("'the' appears {} times", word_count("the").unwrap_or(0));
    println!("'fox' appears {} times", word_count("fox").unwrap_or(0));
    println!("'cat' appears {} times", word_count("cat").unwrap_or(0));
}

Uninitialized State Check

use once_cell::sync::OnceCell;
 
static STATE: OnceCell<String> = OnceCell::new();
 
fn is_initialized() -> bool {
    STATE.get().is_some()
}
 
fn initialize(value: String) -> Result<(), String> {
    STATE.set(value).map_err(|_| "Already initialized".to_string())
}
 
fn get_value() -> Option<&'static str> {
    STATE.get().map(|s| s.as_str())
}
 
fn main() {
    println!("Initialized: {}", is_initialized());
    
    initialize("Hello".to_string()).unwrap();
    println!("Initialized: {}", is_initialized());
    
    // Try to initialize again
    match initialize("World".to_string()) {
        Ok(()) => println!("Initialized"),
        Err(e) => println!("Error: {}", e),
    }
    
    println!("Value: {:?}", get_value());
}

Race-Free Initialization

use once_cell::sync::Lazy;
use std::thread;
 
static DATA: Lazy<Vec<i32>> = Lazy::new(|| {
    println!("Initializing DATA in thread {:?}", thread::current().id());
    thread::sleep(std::time::Duration::from_millis(100));
    vec![1, 2, 3, 4, 5]
});
 
fn main() {
    let handles: Vec<_> = (0..3)
        .map(|i| {
            thread::spawn(move || {
                println!("Thread {} accessing DATA", i);
                let len = DATA.len();
                println!("Thread {} got length: {}", i, len);
            })
        })
        .collect();
    
    for handle in handles {
        handle.join().unwrap();
    }
}

Combining with Serde

// Cargo.toml:
// once_cell = { version = "1.0", features = ["serde"] }
 
use once_cell::sync::Lazy;
use serde::Deserialize;
 
#[derive(Debug, Deserialize)]
struct Settings {
    app_name: String,
    version: String,
}
 
static SETTINGS: Lazy<Settings> = Lazy::new(|| {
    let json = r#"{"app_name":"MyApp","version":"1.0.0"}"#;
    serde_json::from_str(json).expect("Failed to parse settings")
});
 
fn main() {
    println!("App: {} v{}", SETTINGS.app_name, SETTINGS.version);
}

Summary

  • Lazy<T> initializes value on first access, caches forever
  • OnceCell<T> for single-assignment optional initialization
  • Use sync module for thread-safe, unsync for single-threaded
  • Lazy<T> derefs to T for transparent access
  • OnceCell::get_or_init() computes value if not set
  • OnceCell::set() returns Err if already initialized
  • unsync::OnceCell provides get_mut() and take() for mutation
  • No macros needed — cleaner than lazy_static!
  • Perfect for: global config, connection pools, caches, singletons, expensive initialization
  • Thread-safe initialization is race-free — only one thread initializes
  • Prefer once_cell over lazy_static for new projects