How does once_cell::sync::OnceCell::get_or_try_init differ from get_or_init for fallible initialization?

get_or_init is designed for infallible initialization where the closure always succeeds, returning &T directly, while get_or_try_init handles fallible initialization where the closure can fail, returning Result<&T, E> and leaving the cell empty on error so subsequent calls can retry. The key difference is that get_or_try_init doesn't poison the cell on failure—failed initialization can be retried, whereas get_or_init with panics leaves the cell in a permanently poisoned state.

Basic OnceCell Usage

use once_cell::sync::OnceCell;
 
fn basic_usage() {
    // OnceCell: A cell that can be written to once
    let cell: OnceCell<String> = OnceCell::new();
    
    // Check if initialized
    println!("Initialized: {}", cell.get().is_some());  // false
    
    // Initialize with get_or_init
    let value = cell.get_or_init(|| {
        println!("Initializing...");
        String::from("Hello, World!")
    });
    
    println!("Value: {}", value);  // Hello, World!
    println!("Initialized: {}", cell.get().is_some());  // true
    
    // Second call returns existing value, doesn't call closure
    let value2 = cell.get_or_init(|| {
        println!("This won't print");
        String::from("Never used")
    });
    
    assert!(std::ptr::eq(value, value2));  // Same reference
}

OnceCell holds a value that can be initialized exactly once.

get_or_init for Infallible Initialization

use once_cell::sync::OnceCell;
 
fn get_or_init_example() {
    let cell: OnceCell<Vec<i32>> = OnceCell::new();
    
    // get_or_init: Closure returns T directly
    // Signature: fn get_or_init<F: FnOnce() -> T>(&self, f: F) -> &T
    let value = cell.get_or_init(|| {
        // Infallible initialization - always succeeds
        vec![1, 2, 3, 4, 5]
    });
    
    // Returns &T, not Result
    // No error handling needed because closure can't fail
    
    // If closure panics:
    // - Cell is poisoned
    // - Future accesses panic
    
    // Typical use cases:
    // - Static configuration
    // - Computed constants
    // - Cached values
}

get_or_init assumes initialization always succeeds, returning &T directly.

get_or_try_init for Fallible Initialization

use once_cell::sync::OnceCell;
use std::fs::read_to_string;
 
fn get_or_try_init_example() {
    let cell: OnceCell<String> = OnceCell::new();
    
    // get_or_try_init: Closure returns Result<T, E>
    // Signature: fn get_or_try_init<F: FnOnce() -> Result<T, E>>(&self, f: F) -> Result<&T, E>
    let result = cell.get_or_try_init(|| {
        // Fallible initialization - can return Err
        read_to_string("config.txt")
    });
    
    match result {
        Ok(value) => println!("Success: {}", value),
        Err(e) => println!("Failed: {}", e),
    }
    
    // On error, cell remains uninitialized
    // Can retry:
    if result.is_err() {
        // Maybe fix the file, then retry
        let retry = cell.get_or_try_init(|| {
            read_to_string("config.txt")
        });
        println!("Retry result: {:?}", retry);
    }
}

get_or_try_init returns Result<&T, E>, handling initialization that might fail.

Error Recovery Pattern

use once_cell::sync::OnceCell;
use std::io::{self, Error};
 
fn error_recovery() {
    let cell: OnceCell<String> = OnceCell::new();
    
    // Simulated fallible operation
    fn load_config() -> Result<String, io::Error> {
        // Could fail due to:
        // - File not found
        // - Permission denied
        // - Invalid format
        Err(io::Error::new(io::ErrorKind::NotFound, "config not found"))
    }
    
    // First attempt fails
    let first = cell.get_or_try_init(load_config);
    println!("First attempt: {:?}", first.is_err());  // true
    
    // Cell is still uninitialized - can retry
    println!("Cell initialized: {:?}", cell.get());  // None
    
    // Retry with different logic
    let second = cell.get_or_try_init(|| {
        // Maybe use defaults this time
        Ok(String::from("default config"))
    });
    
    println!("Second attempt: {:?}", second);  // Ok("default config")
    println!("Cell now has: {:?}", cell.get());  // Some("default config")
}

Failed initialization leaves the cell empty, allowing retry with different logic.

Comparison with Panic Behavior

use once_cell::sync::OnceCell;
use std::panic;
 
fn panic_behavior() {
    // get_or_init with panic
    let panic_cell: OnceCell<i32> = OnceCell::new();
    
    let result = panic::catch_unwind(|| {
        panic_cell.get_or_init(|| {
            panic!("Initialization failed!");
        });
    });
    
    println!("Panicked: {:?}", result.is_err());  // true
    
    // Cell is poisoned - accessing it panics
    let access = panic::catch_unwind(|| {
        panic_cell.get_or_init(|| 42)  // Panics due to poisoned state
    });
    println!("Access after panic: {:?}", access.is_err());  // true
    
    // get_or_try_init doesn't poison on error
    let try_cell: OnceCell<i32> = OnceCell::new();
    
    let first = try_cell.get_or_try_init::<_, std::convert::Infallible>(|| {
        Err("Failed")?;
        Ok(42)
    });
    println!("First try: {:?}", first);  // Err("Failed")
    
    // Cell is still usable
    let second = try_cell.get_or_try_init(|| Ok(100));
    println!("Second try: {:?}", second);  // Ok(100)
}

get_or_init panics poison the cell permanently; get_or_try_init errors allow retry.

File Loading Example

use once_cell::sync::OnceCell;
use std::fs;
use std::io;
 
struct Config {
    database_url: String,
    api_key: String,
}
 
fn file_loading() -> Result<(), io::Error> {
    let config: OnceCell<Config> = OnceCell::new();
    
    // Attempt to load from file
    let result = config.get_or_try_init(|| {
        let content = fs::read_to_string("config.toml")?;
        // Parse config...
        Ok(Config {
            database_url: String::from("postgres://localhost/db"),
            api_key: String::from("secret"),
        })
    });
    
    match result {
        Ok(cfg) => {
            println!("Database: {}", cfg.database_url);
        }
        Err(e) => {
            eprintln!("Failed to load config: {}", e);
            // Could retry with defaults:
            config.get_or_try_init(|| {
                Ok(Config {
                    database_url: String::from("postgres://localhost/default"),
                    api_key: String::new(),
                })
            }).unwrap();
        }
    }
    
    Ok(())
}

Use get_or_try_init when initialization requires external resources that might fail.

Network Initialization Example

use once_cell::sync::OnceCell;
use std::io;
 
struct Connection {
    connected: bool,
}
 
impl Connection {
    fn connect(addr: &str) -> Result<Self, io::Error> {
        // Simulated connection that might fail
        if addr.is_empty() {
            Err(io::Error::new(io::ErrorKind::NotConnected, "no address"))
        } else {
            Ok(Connection { connected: true })
        }
    }
}
 
fn network_initialization() {
    let conn: OnceCell<Connection> = OnceCell::new();
    
    // Network connections can fail temporarily
    let result = conn.get_or_try_init(|| {
        Connection::connect("")  // Empty address fails
    });
    
    if result.is_err() {
        println!("Connection failed, retrying...");
        
        // Can retry - cell not poisoned
        let retry = conn.get_or_try_init(|| {
            Connection::connect("localhost:8080")
        });
        
        match retry {
            Ok(connection) => println!("Connected!"),
            Err(e) => eprintln!("Still failed: {}", e),
        }
    }
}

Network operations are inherently fallible—get_or_try_init handles transient failures.

Static Initialization

use once_cell::sync::OnceCell;
use std::collections::HashMap;
 
// Static OnceCell for global state
static CONFIG: OnceCell<Config> = OnceCell::new();
static CACHE: OnceCell<HashMap<String, String>> = OnceCell::new();
 
struct Config {
    values: HashMap<String, String>,
}
 
fn static_usage() -> Result<(), std::io::Error> {
    // Initialize static config with get_or_try_init
    CONFIG.get_or_try_init(|| {
        // Load from environment or file
        let mut values = HashMap::new();
        values.insert("key".to_string(), "value".to_string());
        Ok(Config { values })
    })?;
    
    // Subsequent calls return existing value
    let config = CONFIG.get_or_try_init(|| {
        // This closure won't run - already initialized
        Ok(Config { values: HashMap::new() })
    })?;
    
    // get_or_init for infallible static
    CACHE.get_or_init(|| {
        let mut map = HashMap::new();
        map.insert("cached".to_string(), "data".to_string());
        map
    });
    
    Ok(())
}

Use get_or_try_init for static cells that require fallible initialization.

Lazy Static Alternative

use once_cell::sync::OnceCell;
use std::fs;
 
// Using OnceCell with get_or_try_init
static DATA: OnceCell<Vec<u8>> = OnceCell::new();
 
fn lazy_static() -> Result<(), std::io::Error> {
    // Initialize on first use
    let data = DATA.get_or_try_init(|| {
        fs::read("data.bin")
    })?;
    
    // Use data...
    println!("Loaded {} bytes", data.len());
    
    Ok(())
}
 
// Compare with LazyCell (infallible)
use once_cell::sync::Lazy;
 
static INFALLIBLE_DATA: Lazy<Vec<u8>> = Lazy::new(|| {
    // Must succeed - panics on failure
    fs::read("data.bin").unwrap_or_default()
});

OnceCell with get_or_try_init provides error handling; Lazy forces infallible initialization.

Multiple Retry Strategies

use once_cell::sync::OnceCell;
use std::io;
 
fn retry_strategies() {
    let cell: OnceCell<String> = OnceCell::new();
    
    // Strategy 1: Retry same operation
    fn retry_same() {
        let cell: OnceCell<String> = OnceCell::new();
        let mut attempts = 0;
        
        let result = loop {
            attempts += 1;
            match cell.get_or_try_init(|| {
                if attempts < 3 {
                    Err(io::Error::new(io::ErrorKind::TimedOut, "timeout"))
                } else {
                    Ok(String::from("success"))
                }
            }) {
                Ok(v) => break Ok(v),
                Err(e) if attempts < 3 => continue,
                Err(e) => break Err(e),
            }
        };
    }
    
    // Strategy 2: Fallback to defaults
    fn with_fallback() {
        let cell: OnceCell<String> = OnceCell::new();
        
        let result = cell.get_or_try_init(|| {
            fs::read_to_string("config.txt")
                .or_else(|_| Ok(String::from("default config")))
        });
    }
    
    // Strategy 3: Multiple sources
    fn multiple_sources() {
        let cell: OnceCell<String> = OnceCell::new();
        
        let result = cell.get_or_try_init(|| {
            // Try env var first
            std::env::var("CONFIG")
                // Then try file
                .or_else(|_| fs::read_to_string("config.txt"))
                // Then use default
                .or_else(|_| Ok(String::from("default")))
        });
    }
}

get_or_try_init enables various retry and fallback strategies.

Threading Behavior

use once_cell::sync::OnceCell;
use std::thread;
use std::sync::Arc;
 
fn threading_behavior() {
    let cell = Arc::new(OnceCell::<i32>::new());
    
    let mut handles = vec![];
    
    for i in 0..3 {
        let cell = Arc::clone(&cell);
        let handle = thread::spawn(move || {
            // All threads call get_or_try_init
            // Only first thread's closure runs
            let result = cell.get_or_try_init(|| {
                println!("Thread {} initializing", i);
                if i == 0 {
                    // First thread fails
                    Err("Simulated error")
                } else {
                    Ok(42)
                }
            });
            
            println!("Thread {} got: {:?}", i, result);
            result
        });
        handles.push(handle);
    }
    
    // Key behaviors:
    // 1. Only one thread runs the closure
    // 2. Other threads wait for result
    // 3. On Err, waiting threads see the error
    // 4. Cell remains uninitialized after error
    // 5. Subsequent calls can retry
    
    for handle in handles {
        let _ = handle.join();
    }
}

Both methods provide thread-safe initialization with blocking on concurrent access.

Type Signatures Compared

use once_cell::sync::OnceCell;
 
fn type_signatures() {
    // get_or_init signature:
    // fn get_or_init<F>(&self, f: F) -> &T
    // where
    //     F: FnOnce() -> T
    
    // - Closure returns T (not Result)
    // - Returns &T (not Result)
    // - Panics propagate and poison cell
    // - No error handling possible
    
    // get_or_try_init signature:
    // fn get_or_try_init<F, E>(&self, f: F) -> Result<&T, E>
    // where
    //     F: FnOnce() -> Result<T, E>
    
    // - Closure returns Result<T, E>
    // - Returns Result<&T, E>
    // - Errors don't poison cell
    // - Can retry after error
    
    // When to use which:
    // - Use get_or_init when initialization cannot fail
    // - Use get_or_try_init when initialization might fail
}

The type signatures reflect their intended use cases.

Error Types

use once_cell::sync::OnceCell;
use std::io;
 
fn error_types() {
    // Specific error types
    let cell: OnceCell<String> = OnceCell::new();
    let result: Result<&String, io::Error> = cell.get_or_try_init(|| {
        fs::read_to_string("file.txt")
    });
    
    // Custom error types
    #[derive(Debug)]
    enum InitError {
        Io(io::Error),
        Parse(String),
    }
    
    impl From<io::Error> for InitError {
        fn from(e: io::Error) -> Self {
            InitError::Io(e)
        }
    }
    
    let cell2: OnceCell<i32> = OnceCell::new();
    let result: Result<&i32, InitError> = cell2.get_or_try_init(|| {
        let content = fs::read_to_string("number.txt")?;
        let number = content.trim().parse::<i32>()
            .map_err(|e| InitError::Parse(e.to_string()))?;
        Ok(number)
    });
    
    // Box<dyn Error> for flexibility
    let cell3: OnceCell<String> = OnceCell::new();
    let result: Result<&String, Box<dyn std::error::Error>> = 
        cell3.get_or_try_init(|| {
            let content = fs::read_to_string("file.txt")?;
            Ok(content)
        });
}

Any error type implementing Debug can be used with get_or_try_init.

Memory Poisoning

use once_cell::sync::OnceCell;
use std::panic::{self, AssertUnwindSafe};
 
fn memory_poisoning() {
    // get_or_init poisoning
    let poisoned_cell: OnceCell<i32> = OnceCell::new();
    
    let _ = panic::catch_unwind(AssertUnwindSafe(|| {
        poisoned_cell.get_or_init(|| {
            panic!("Intentional panic during init");
        });
    }));
    
    // Cell is poisoned - future access panics
    let result = panic::catch_unwind(|| {
        poisoned_cell.get_or_init(|| 42)
    });
    assert!(result.is_err());
    
    // get_or_try_init doesn't poison on error
    let try_cell: OnceCell<i32> = OnceCell::new();
    
    let result = try_cell.get_or_try_init::<_, &str>(|| {
        Err("Intentional error")
    });
    assert!(result.is_err());
    
    // Cell is NOT poisoned - can still use
    let result = try_cell.get_or_try_init(|| Ok(42));
    assert!(result.is_ok());
}

get_or_try_init errors don't poison; get_or_init panics do.

Sync and Send Requirements

use once_cell::sync::OnceCell;
 
fn sync_send_requirements() {
    // once_cell::sync::OnceCell requires T: Send
    // (for safe concurrent access)
    
    // This works:
    let cell: OnceCell<String> = OnceCell::new();
    // String: Send
    
    // This also works:
    let cell2: OnceCell<Vec<i32>> = OnceCell::new();
    // Vec<i32>: Send
    
    // For non-Send types, use once_cell::unsync::OnceCell
    // But that's single-threaded only
    
    // sync::OnceCell provides thread-safe lazy initialization
    // unsync::OnceCell is for single-threaded use
}

sync::OnceCell requires T: Send for thread safety.

Practical Patterns

use once_cell::sync::OnceCell;
use std::io;
 
// Pattern 1: Database pool with retry
struct Pool {
    connections: Vec<String>,
}
 
impl Pool {
    fn connect(url: &str) -> Result<Self, io::Error> {
        // Connection might fail
        Ok(Pool { connections: vec![] })
    }
}
 
static DB_POOL: OnceCell<Pool> = OnceCell::new();
 
fn get_pool() -> Result<&'static Pool, io::Error> {
    DB_POOL.get_or_try_init(|| {
        let url = std::env::var("DATABASE_URL")
            .unwrap_or_else(|_| "localhost:5432".to_string());
        Pool::connect(&url)
    })
}
 
// Pattern 2: Configuration with validation
struct Config {
    port: u16,
}
 
impl Config {
    fn load() -> Result<Self, String> {
        let port = std::env::var("PORT")
            .unwrap_or_else(|_| "8080".to_string())
            .parse::<u16>()
            .map_err(|e| format!("Invalid port: {}", e))?;
        
        if port < 1024 {
            return Err("Port must be >= 1024".to_string());
        }
        
        Ok(Config { port })
    }
}
 
static CONFIG: OnceCell<Config> = OnceCell::new();
 
fn get_config() -> Result<&'static Config, String> {
    CONFIG.get_or_try_init(Config::load)
}
 
// Pattern 3: TLS configuration
fn tls_config() -> Result<&'static rustls::ServerConfig, rustls::Error> {
    static TLS_CONFIG: OnceCell<rustls::ServerConfig> = OnceCell::new();
    
    TLS_CONFIG.get_or_try_init(|| {
        // Load certificates, configure TLS
        // This can fail if certs are invalid
        todo!()
    })
}

Use get_or_try_init for any initialization that can fail: file I/O, network connections, parsing.

Synthesis

Quick comparison:

Aspect get_or_init get_or_try_init
Closure returns T Result<T, E>
Returns &T Result<&T, E>
On panic Poisons cell N/A (no panic)
On error N/A (no error) Returns Err, cell empty
Retry possible No (after panic) Yes
Use case Infallible init Fallible init

Decision guide:

// Use get_or_init when:
// - Initialization cannot fail
// - You have a default/computed value
// - Panics are acceptable (will poison)
 
let cell: OnceCell<Vec<i32>> = OnceCell::new();
let value = cell.get_or_init(|| vec![1, 2, 3]);
 
// Use get_or_try_init when:
// - Initialization might fail
// - External resources required (files, network)
// - Validation might reject input
// - You want to retry after failure
 
let cell: OnceCell<Config> = OnceCell::new();
let value = cell.get_or_try_init(|| {
    let file = fs::read_to_string("config.toml")?;
    parse_config(&file)
});

Key insight: get_or_try_init provides the crucial ability to recover from initialization failures. When get_or_init's closure panics, the cell is permanently poisoned—any future access panics immediately. When get_or_try_init's closure returns Err, the error propagates to the caller and the cell remains uninitialized, allowing subsequent calls to attempt initialization again. This makes get_or_try_init essential for any lazy initialization involving I/O, network requests, parsing, or validation—operations that fail transiently or permanently. The closure only runs once per initialization attempt (other threads block during initialization), and the returned &T lives as long as the cell, making get_or_try_init suitable for static lazy initialization with proper error handling.