How do I initialize values once with once_cell in Rust?

Walkthrough

The once_cell crate provides two types for lazy initialization: Lazy<T> for values that are initialized on first access, and OnceCell<T> for values that may or may not be set. These types are thread-safe and eliminate the need for messy static mut patterns or manual synchronization. They're perfect for global configuration, singleton patterns, cached computations, and any scenario where you need "initialize once, access many times" semantics.

Key concepts:

  1. Lazy — value initialized automatically on first access
  2. OnceCell — value set explicitly, at most once
  3. Thread safetysync module provides thread-safe versions
  4. No macros needed — cleaner than lazy_static!
  5. Status queries — check if value has been initialized

Code Example

# Cargo.toml
[dependencies]
once_cell = "1.0"
use once_cell::sync::Lazy;
 
// Global configuration, initialized on first access
static CONFIG: Lazy<Vec<String>> = Lazy::new(|| {
    println!("Initializing config...");
    vec!["config1".to_string(), "config2".to_string()]
});
 
fn main() {
    println!("Before accessing CONFIG");
    println!("Config: {:?}", *CONFIG);
    println!("After accessing CONFIG");
    
    // Second access doesn't reinitialize
    println!("Config again: {:?}", *CONFIG);
}

Lazy Static Initialization

use once_cell::sync::Lazy;
use std::collections::HashMap;
 
// Simple lazy static
static NUMBER: Lazy<i32> = Lazy::new(|| {
    println!("Computing NUMBER...");
    42
});
 
// Lazy static with complex initialization
static WORD_COUNTS: Lazy<HashMap<&'static str, usize>> = Lazy::new(|| {
    let text = "the quick brown fox jumps over the lazy dog";
    let mut counts = HashMap::new();
    for word in text.split_whitespace() {
        *counts.entry(word).or_insert(0) += 1;
    }
    counts
});
 
// Lazy static that requires computation
static PRIMES: Lazy<Vec<u64>> = Lazy::new(|| {
    (2..100).filter(|&n| is_prime(n)).collect()
});
 
fn is_prime(n: u64) -> bool {
    if n < 2 { return false; }
    (2..=(n as f64).sqrt() as u64).all(|i| n % i != 0)
}
 
fn main() {
    println!("Starting...");
    
    // NUMBER not initialized yet
    println!("About to access NUMBER");
    
    // Initialize on first access
    println!("NUMBER = {}", *NUMBER);
    
    // Already initialized, won't print "Computing..." again
    println!("NUMBER again = {}", *NUMBER);
    
    // Access lazy HashMap
    println!("\nWord counts: {:?}", *WORD_COUNTS);
    
    // Access lazy Vec
    println!("\nPrimes under 100: {:?}", *PRIMES);
}

OnceCell for Optional Values

use once_cell::sync::OnceCell;
 
static INSTANCE: OnceCell<String> = OnceCell::new();
 
fn main() {
    // Check if set
    println!("Is set: {}", INSTANCE.get().is_some());
    
    // Set the value (can only be done once)
    let result = INSTANCE.set("Hello, World!".to_string());
    println!("Set result: {:?}", result); // Ok(())
    
    // Try to set again
    let result = INSTANCE.set("Goodbye".to_string());
    println!("Set again result: {:?}", result); // Err(value)
    
    // Get the value
    if let Some(value) = INSTANCE.get() {
        println!("Value: {}", value);
    }
    
    // get_or_init initializes if not set
    static ANOTHER: OnceCell<Vec<i32>> = OnceCell::new();
    let value = ANOTHER.get_or_init(|| vec![1, 2, 3]);
    println!("Another: {:?}", value);
}

Basic OnceCell Operations

use once_cell::sync::OnceCell;
 
fn main() {
    let cell: OnceCell<String> = OnceCell::new();
    
    // Check if initialized
    println!("Initialized: {}", cell.get().is_some());
    
    // Set value
    cell.set("first".to_string()).unwrap();
    println!("After set: {:?}", cell.get());
    
    // Try to set again (fails)
    match cell.set("second".to_string()) {
        Ok(()) => println!("Set succeeded"),
        Err(value) => println!("Set failed, value was: {}", value),
    }
    
    // Get or initialize
    let cell2: OnceCell<i32> = OnceCell::new();
    let value = cell2.get_or_init(|| {
        println!("Computing value...");
        42
    });
    println!("Value: {}", value);
    
    // Get or try to initialize (may fail)
    let cell3: OnceCell<i32> = OnceCell::new();
    let result = cell3.get_or_try_init(|| -> Result<i32, &'static str> {
        Ok(100)
    });
    println!("Result: {:?}", result);
}

Thread-Safe vs Unsynchronized Versions

use once_cell::{sync::Lazy, unsync};
 
// sync::Lazy - thread-safe, can be used in statics
static GLOBAL: Lazy<i32> = Lazy::new(|| {
    println!("Initializing GLOBAL");
    42
});
 
// unsync::Lazy - not thread-safe, but slightly faster
// Cannot be used in statics, only in local variables
fn local_lazy() {
    let local: unsync::Lazy<i32> = unsync::Lazy::new(|| {
        println!("Initializing local");
        100
    });
    
    println!("Local: {}", *local);
}
 
fn main() {
    // Thread-safe version
    println!("Global: {}", *GLOBAL);
    
    // Unsynchronized version
    local_lazy();
    
    // OnceCell comparison
    let sync_cell: once_cell::sync::OnceCell<i32> = once_cell::sync::OnceCell::new();
    let unsync_cell: once_cell::unsync::OnceCell<i32> = once_cell::unsync::OnceCell::new();
    
    sync_cell.set(1).unwrap();
    unsync_cell.set(2).unwrap();
    
    println!("Sync cell: {:?}", sync_cell.get());
    println!("Unsync cell: {:?}", unsync_cell.get());
}

Global Configuration Pattern

use once_cell::sync::{Lazy, OnceCell};
use std::env;
 
#[derive(Debug)]
struct Config {
    database_url: String,
    api_key: String,
    max_connections: usize,
    debug: bool,
}
 
static CONFIG: OnceCell<Config> = OnceCell::new();
 
fn get_config() -> &'static Config {
    CONFIG.get_or_init(|| {
        Config {
            database_url: env::var("DATABASE_URL")
                .unwrap_or_else(|_| "localhost:5432".to_string()),
            api_key: env::var("API_KEY")
                .unwrap_or_else(|_| "default-key".to_string()),
            max_connections: env::var("MAX_CONNECTIONS")
                .ok()
                .and_then(|s| s.parse().ok())
                .unwrap_or(10),
            debug: env::var("DEBUG").is_ok(),
        }
    })
}
 
fn main() {
    // First call initializes the config
    let config = get_config();
    println!("Config: {:?}", config);
    
    // Subsequent calls return the same instance
    let config2 = get_config();
    assert!(std::ptr::eq(config, config2));
    println!("Same instance: {}", std::ptr::eq(config, config2));
}

Singleton Pattern

use once_cell::sync::Lazy;
use std::collections::HashMap;
 
struct Cache {
    data: HashMap<String, String>,
}
 
impl Cache {
    fn new() -> Self {
        println!("Creating cache singleton...");
        let mut data = HashMap::new();
        data.insert("key1".to_string(), "value1".to_string());
        Self { data }
    }
    
    fn get(&self, key: &str) -> Option<&String> {
        self.data.get(key)
    }
    
    fn insert(&mut self, key: String, value: String) {
        self.data.insert(key, value);
    }
}
 
// Global singleton instance
static CACHE: Lazy<std::sync::Mutex<Cache>> = Lazy::new(|| {
    std::sync::Mutex::new(Cache::new())
});
 
fn main() {
    // Access the singleton
    {
        let cache = CACHE.lock().unwrap();
        println!("key1: {:?}", cache.get("key1"));
    }
    
    // Modify the singleton
    {
        let mut cache = CACHE.lock().unwrap();
        cache.insert("key2".to_string(), "value2".to_string());
    }
    
    // Access again
    {
        let cache = CACHE.lock().unwrap();
        println!("key2: {:?}", cache.get("key2"));
    }
}

Lazy Regex Compilation

use once_cell::sync::Lazy;
use regex::Regex;
 
// Compile regex once, use many times
static EMAIL_REGEX: Lazy<Regex> = Lazy::new(|| {
    Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$").unwrap()
});
 
static PHONE_REGEX: Lazy<Regex> = Lazy::new(|| {
    Regex::new(r"^\d{3}-\d{3}-\d{4}$").unwrap()
});
 
fn is_valid_email(email: &str) -> bool {
    EMAIL_REGEX.is_match(email)
}
 
fn is_valid_phone(phone: &str) -> bool {
    PHONE_REGEX.is_match(phone)
}
 
fn main() {
    let emails = vec!["test@example.com", "invalid-email", "user@domain.org"];
    
    for email in emails {
        println!("{}: valid email = {}", email, is_valid_email(email));
    }
    
    let phones = vec!["123-456-7890", "1234567890", "123-45-6789"];
    
    for phone in phones {
        println!("{}: valid phone = {}", phone, is_valid_phone(phone));
    }
}

Expensive Computation Caching

use once_cell::sync::{Lazy, OnceCell};
use std::time::Instant;
 
// Fibonacci sequence cache
static FIB_CACHE: Lazy<std::sync::Mutex<Vec<u64>>> = Lazy::new(|| {
    std::sync::Mutex::new(vec![0, 1])
});
 
fn fibonacci(n: usize) -> u64 {
    let mut cache = FIB_CACHE.lock().unwrap();
    
    while cache.len() <= n {
        let next = cache[cache.len() - 1] + cache[cache.len() - 2];
        cache.push(next);
    }
    
    cache[n]
}
 
// Heavy computation result
static HEAVY_RESULT: OnceCell<String> = OnceCell::new();
 
fn get_heavy_result() -> &'static str {
    HEAVY_RESULT.get_or_init(|| {
        println!("Performing heavy computation...");
        std::thread::sleep(std::time::Duration::from_millis(500));
        "Computed result".to_string()
    })
}
 
fn main() {
    // First call computes
    let start = Instant::now();
    let result = get_heavy_result();
    println!("First call: {} ({:?})", result, start.elapsed());
    
    // Second call uses cached value
    let start = Instant::now();
    let result = get_heavy_result();
    println!("Second call: {} ({:?})", result, start.elapsed());
    
    // Fibonacci caching
    println!("\nFibonacci:");
    for n in [10, 20, 30, 10, 20] {
        println!("  fib({}) = {}", n, fibonacci(n));
    }
}

Database Connection Pool

use once_cell::sync::Lazy;
use std::sync::Mutex;
 
// Simulated connection
#[derive(Debug)]
struct Connection {
    id: usize,
}
 
impl Connection {
    fn new(id: usize) -> Self {
        println!("Creating connection {}", id);
        Self { id }
    }
    
    fn query(&self, sql: &str) -> String {
        format!("[Conn {}] Result of: {}", self.id, sql)
    }
}
 
struct ConnectionPool {
    connections: Vec<Connection>,
    next_id: usize,
}
 
impl ConnectionPool {
    fn new(size: usize) -> Self {
        println!("Creating connection pool with {} connections", size);
        let connections = (0..size).map(|i| Connection::new(i)).collect();
        Self { connections, next_id: 0 }
    }
    
    fn get(&mut self) -> &Connection {
        let conn = &self.connections[self.next_id % self.connections.len()];
        self.next_id += 1;
        conn
    }
}
 
// Global connection pool
static POOL: Lazy<Mutex<ConnectionPool>> = Lazy::new(|| {
    Mutex::new(ConnectionPool::new(5))
});
 
fn query(sql: &str) -> String {
    let mut pool = POOL.lock().unwrap();
    pool.get().query(sql)
}
 
fn main() {
    println!("Starting application...");
    
    // Pool created on first query
    let result = query("SELECT * FROM users");
    println!("Result: {}", result);
    
    // Reuse existing pool
    let result = query("SELECT * FROM orders");
    println!("Result: {}", result);
}

Environment Variable Parsing

use once_cell::sync::Lazy;
use std::env;
 
struct EnvConfig {
    host: String,
    port: u16,
    debug: bool,
}
 
static ENV: Lazy<EnvConfig> = Lazy::new(|| {
    EnvConfig {
        host: env::var("HOST").unwrap_or_else(|_| "localhost".to_string()),
        port: env::var("PORT")
            .ok()
            .and_then(|p| p.parse().ok())
            .unwrap_or(8080),
        debug: env::var("DEBUG").is_ok(),
    }
});
 
fn main() {
    // Environment parsed once
    println!("Host: {}", ENV.host);
    println!("Port: {}", ENV.port);
    println!("Debug: {}", ENV.debug);
    
    // Same values on subsequent access
    println!("\nConfig is consistent:");
    println!("Host again: {}", ENV.host);
}

Thread-Local Storage

use once_cell::sync::Lazy;
use std::cell::RefCell;
 
thread_local! {
    static COUNTER: RefCell<i32> = RefCell::new(0);
}
 
// Thread-local lazy initialization
static THREAD_DATA: Lazy<std::thread::LocalKey<RefCell<Vec<String>>>> = Lazy::new(|| {
    std::thread_local! {
        static DATA: RefCell<Vec<String>> = RefCell::new(Vec::new());
    }
    DATA
});
 
fn increment_counter() -> i32 {
    COUNTER.with(|c| {
        let mut counter = c.borrow_mut();
        *counter += 1;
        *counter
    })
}
 
fn main() {
    println!("Main thread counter:");
    for _ in 0..3 {
        println!("  {}", increment_counter());
    }
    
    // Spawn a new thread - has its own counter
    let handle = std::thread::spawn(|| {
        println!("\nChild thread counter:");
        for _ in 0..3 {
            println!("  {}", increment_counter());
        }
    });
    
    handle.join().unwrap();
    
    println!("\nMain thread counter again:");
    println!("  {}", increment_counter());
}

Checking Initialization Status

use once_cell::sync::OnceCell;
 
static STATUS: OnceCell<String> = OnceCell::new();
 
fn main() {
    // Check if initialized
    println!("Initialized: {}", STATUS.get().is_some());
    
    // Get current status (None if not set)
    println!("Current: {:?}", STATUS.get());
    
    // Set the value
    STATUS.set("ready".to_string()).unwrap();
    
    // Now it's initialized
    println!("Initialized: {}", STATUS.get().is_some());
    println!("Current: {:?}", STATUS.get());
    
    // get_or_init with condition
    static OPTIONAL: OnceCell<i32> = OnceCell::new();
    
    if let Some(value) = OPTIONAL.get() {
        println!("Optional already set: {}", value);
    } else {
        println!("Optional not set yet");
        OPTIONAL.set(42).ok();
    }
}

Integration with Serde

// Requires: once_cell = { version = "1.0", features = ["serde"] }
 
use once_cell::sync::Lazy;
use serde::Deserialize;
 
#[derive(Debug, Deserialize)]
struct Settings {
    name: String,
    version: String,
    features: Vec<String>,
}
 
static SETTINGS: Lazy<Settings> = Lazy::new(|| {
    let json = r#"
        {
            "name": "my-app",
            "version": "1.0.0",
            "features": ["auth", "logging", "cache"]
        }
    "#;
    serde_json::from_str(json).expect("Failed to parse settings")
});
 
fn main() {
    println!("App: {} v{}", SETTINGS.name, SETTINGS.version);
    println!("Features: {:?}", SETTINGS.features);
}

Comparison: Lazy vs OnceCell

use once_cell::sync::{Lazy, OnceCell};
 
// Lazy: automatically initialized on first access
static AUTO: Lazy<String> = Lazy::new(|| {
    println!("Lazy initializing AUTO");
    "auto-initialized".to_string()
});
 
// OnceCell: explicitly set once
static MANUAL: OnceCell<String> = OnceCell::new();
 
fn main() {
    println!("=== Lazy ===");
    println!("Before access");
    println!("Value: {}", *AUTO); // Initializes here
    println!("After access");
    
    println!("\n=== OnceCell ===");
    println!("Before set: {:?}", MANUAL.get());
    MANUAL.set("manually-set".to_string()).unwrap();
    println!("After set: {:?}", MANUAL.get());
    
    // Use Lazy when:
    // - You always need the value
    // - You want automatic initialization
    // - The initialization is deterministic
    
    // Use OnceCell when:
    // - The value may never be needed
    // - You need explicit control over when it's set
    // - Initialization may fail or be conditional
}

Error Handling with get_or_try_init

use once_cell::sync::OnceCell;
use std::fs;
 
static FILE_CONTENTS: OnceCell<String> = OnceCell::new();
 
fn load_file() -> Result<&'static str, std::io::Error> {
    FILE_CONTENTS.get_or_try_init(|| {
        // Simulated file read
        println!("Loading file...");
        // In real code: fs::read_to_string("config.txt")
        Ok("file contents here".to_string())
    }).map(|s| s.as_str())
}
 
fn main() {
    // First call loads the file
    match load_file() {
        Ok(contents) => println!("Contents: {}", contents),
        Err(e) => println!("Error: {}", e),
    }
    
    // Subsequent calls return cached value
    match load_file() {
        Ok(contents) => println!("Cached: {}", contents),
        Err(e) => println!("Error: {}", e),
    }
}

Real-World Example: Application State

use once_cell::sync::{Lazy, OnceCell};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
 
#[derive(Debug)]
struct AppState {
    users: HashMap<u64, String>,
    sessions: HashMap<String, u64>,
    config: Config,
}
 
#[derive(Debug, Clone)]
struct Config {
    app_name: String,
    version: String,
}
 
static STATE: Lazy<Arc<Mutex<AppState>>> = Lazy::new(|| {
    println!("Initializing application state...");
    Arc::new(Mutex::new(AppState {
        users: HashMap::new(),
        sessions: HashMap::new(),
        config: Config {
            app_name: "MyApp".to_string(),
            version: "1.0.0".to_string(),
        },
    }))
});
 
static SHUTDOWN_FLAG: OnceCell<bool> = OnceCell::new();
 
fn register_user(name: String) -> u64 {
    let mut state = STATE.lock().unwrap();
    let id = state.users.len() as u64 + 1;
    state.users.insert(id, name);
    id
}
 
fn get_user(id: u64) -> Option<String> {
    let state = STATE.lock().unwrap();
    state.users.get(&id).cloned()
}
 
fn is_shutting_down() -> bool {
    SHUTDOWN_FLAG.get().copied().unwrap_or(false)
}
 
fn initiate_shutdown() {
    SHUTDOWN_FLAG.set(true).ok();
}
 
fn main() {
    // State initialized on first access
    let id1 = register_user("Alice".to_string());
    let id2 = register_user("Bob".to_string());
    
    println!("Registered users:");
    println!("  {}: {:?}", id1, get_user(id1));
    println!("  {}: {:?}", id2, get_user(id2));
    
    // Check shutdown status
    println!("\nShutting down: {}", is_shutting_down());
    
    // Initiate shutdown
    initiate_shutdown();
    println!("Shutting down: {}", is_shutting_down());
}

Migration from lazy_static!

// OLD: using lazy_static!
// lazy_static::lazy_static! {
//     static ref CONFIG: Vec<String> = {
//         vec!["value1".to_string(), "value2".to_string()]
//     };
// }
 
// NEW: using once_cell
use once_cell::sync::Lazy;
 
static CONFIG: Lazy<Vec<String>> = Lazy::new(|| {
    vec!["value1".to_string(), "value2".to_string()]
});
 
fn main() {
    println!("Config: {:?}", *CONFIG);
    
    // Benefits of once_cell over lazy_static:
    // 1. No macro needed - standard Rust syntax
    // 2. Better IDE support and documentation
    // 3. Works with generic types
    // 4. More control with OnceCell
}

Summary

  • Lazy<T> automatically initializes on first access — use for values that are always needed
  • OnceCell<T> requires explicit set() — use for values that may or may not be set
  • sync module provides thread-safe versions for use in static items
  • unsync module provides faster non-thread-safe versions for local use
  • get_or_init() returns existing value or initializes with closure
  • get_or_try_init() supports fallible initialization with Result
  • get() returns Option<&T> to check initialization status
  • Combine with Mutex for mutable global state
  • Use for: configuration, singletons, regex compilation, cached computations, connection pools
  • Cleaner alternative to lazy_static! macro — no macros needed
  • Note: std::sync::OnceLock and std::sync::LazyLock are available in Rust 1.70+ as standard library alternatives