How to properly use `OnceLock` with `Result`s?

(First time OnceLock user)

In the below snippet, since the get_or_init function of OnceLock returns a &T (thus &Result<T, E> ), but later usage of the value may need to bubble up the error value, I'd like to turn it into Result<&T, E>.

But Error is not Clone due to reqwest::Error and std::io::Error are not Clone. Thus cache_file.as_deref().map_err(|err| err.clone()) won't work. I know boxing those errors could make Clone derivable on Error.

I considered to change the return value of get_or_init to &Option<Path> (thus return value of cache_file can be Option<&Path> by calling as_ref then I don't need to deal with the &Error part. But then every later usage of the value would need a match/if let guard to get the path instead of propagating the error directly.

How to fix this? Or what's the best practice of this kind of static variable with Result or Option types?

Thanks.

use std::{
    path::{Path, PathBuf},
    sync::OnceLock,
};

#[derive(Debug, thiserror::Error)]
pub enum Error {
    #[error("failed to fetch data")]
    Fetch(#[source] reqwest::Error),

    #[error("failed to access cache dir")]
    CacheDir,

    #[error("Other IO error")]
    IO(#[from] std::io::Error),
}

static CACHE_FILE: OnceLock<Result<PathBuf, Error>> = OnceLock::new();

pub fn cache_file() -> Result<&'static Path, &'static Error> {
    let cache_file = CACHE_FILE.get_or_init(|| {
        dirs::cache_dir()
            .map(|dir| dir.join("file.json"))
            .ok_or(Error::CacheDir)
    });

    cache_file.as_deref()
}

// Maybe another option
// static CACHE_FILE: OnceLock<Option<PathBuf>> = OnceLock::new();
// pub fn cache_file() -> Option<&'static Path> {
//     let cache_file = CACHE_FILE2.get_or_init(|| dirs::cache_dir().map(|dir| dir.join("file.json")));
// 
//     cache_file.as_deref()
// }


pub fn read_cache() -> Result<String, Error> {
    // error: ^ the trait `From<&Error>` is not implemented for `Error`
    let file = cache_file()?;

    Ok(std::fs::read_to_string(file)?)
}

fn main() {}

You may consider storing a reference to an Error within an Error like this. You may want to use a struct wrapping an enum for your errors rather than an enum if you want to hide this implementation detail.

#[derive(Debug, thiserror::Error)]
pub enum Error {
    #[error("failed to fetch data")]
    Fetch(#[source] reqwest::Error),

    #[error("failed to access cache dir")]
    CacheDir,

    #[error("Other IO error")]
    IO(#[from] std::io::Error),
    
    #[error(transparent)]
    ErrorRef(#[from] &'static Error),
}

If you want to stick to the standard library's version of the once_cell types and you can use nightly, I'd use OnceLock::get_or_try_init, which is the version of the get_or_init method for initialisation callbacks that may fail. If you can't use nightly, use once_cell::sync::OnceCell, which is the same as std::sync::OnceLock. Here an example of using once_cell::sync::OnceCell::get_or_try_init:

use std::path::PathBuf;

use once_cell::sync::OnceCell;

#[derive(Debug, thiserror::Error)]
pub enum Error {
    #[error("failed to fetch data")]
    Fetch(#[source] reqwest::Error),

    #[error("failed to access cache dir")]
    CacheDir,

    #[error("Other IO error")]
    IO(#[from] std::io::Error),
}

static CACHE_FILE: OnceCell<PathBuf> = OnceCell::new();

pub fn cache_file() -> Result<&'static PathBuf, Error> {
    CACHE_FILE.get_or_try_init(|| Some(PathBuf::from("/tmp")).ok_or(Error::CacheDir))
}

pub fn read_cache() -> Result<String, Error> {
    let file = cache_file()?;

    Ok(std::fs::read_to_string(file)?)
}

fn main() {}

Playground.

2 Likes

Storing the reference to error works like boxing it here?

Thanks for pointing me to get_or_try_init! I missed it when I searched through the docs. :sweat_smile:

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.