The opinions expressed on this blog are mine alone

Propagating and converting Rust errors while passing context

2024-04-17

Recently I wanted to use std::env::var to read environment variables from my application. I was suprised to see that when the function returns a VarError::NotPresent error, the name of the missing environment variable is not included in the error message. I wanted to display a much more user friendly error message, so I looked at the documentation:

                  
use std::env;

let key = "HOME";
match env::var(key) {
    Ok(val) => println!("{key}: {val:?}"),
    Err(e) => println!("couldn't interpret {key}: {e}"),
}
                  
                

Easy enough, but there is one problem though. What if I want to return a custom error, and later in the chain propagate it upwards with the ? operator? How to pass the key and preserve it while converting the error to something else? It took me a bit of digging to come up with a solution I am satisfied with:

                  
 1     use std::env;
 2     use std::env::VarError;
 3    
 4     #[derive(Debug, Eq, Hash, PartialEq)]
 5     enum EnvVar {
 6         AwsAccessKeyId,
 7     }
 8    
 9     // Converting EnvVar enum keys to &str type (line #36)
10     impl From for &str {
11       fn from(key: EnvVar) -> Self {
12         match key {
13           EnvVar::AwsAccessKeyId => "AWS_ACCESS_KEY_ID",
14         }
15       }
16     }
17    
18     #[derive(Debug)]
19     pub enum ClientError<'a> {
20       CredentialNotSet(&'a str, VarError)
21     }
22
23     // Human readable display of ClientError.
24     // CredentialNotSet receives both the key, both the original env::VarError.
25     impl fmt::Display for ClientError<'_> {
26       fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
27         match self {
28           ClientError::CredentialNotSet(key, err) => {
29             write!(f, "{} {}", key, err)
30           }
31         }
32       }
33     }
34    
35     fn env_var(key: EnvVar) -> Result> {
36       let key_str: &str = key.into();
37
38       // Mapping env::VarError to our own ClientError::CredentialNotSet
39       // by passing the missing env var key and the original error.
40       env::var(key_str).map_err(|e| ClientError::CredentialNotSet(key_str, e))
41     }
42    
43     fn get_aws_credentials(config: &Configuration) -> Result> {
44       ...
45    
46       Ok(CredentialsStore(
47         ...
48         // ClientError can be easily propagated with the ? operator
49         env_var(EnvVar::AwsAccessKeyId)?,
50       ))
51     }
                  
                

Finally, if the expected environment variables are unset, the error message includes the missing variable name: AWS_ACCESS_KEY_ID environment variable not found.