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.
