3

I'm struggling with error handling cleanly in Rust. Say I have a function that is propogating multiple error types with Box<dyn Error>. To unwrap and handle the error, I'm doing the following:

fn main() {
    let json =
        get_json_response(format!("{}{}", BASE_URL, LOGIN_URL).as_str()).unwrap_or_else(|e| {
            eprintln!("Error: failed to get: {}", e);
            std::process::exit(1);
        });
}

fn get_json_response(url: &str) -> Result<Value, Box<dyn Error>> {
    let resp = ureq::get(url)
        .set("Authorization", format!("Bearer {}", API_TOKEN).as_str())
        .call()?
        .into_json()?;
    Ok(resp)
}

This works well. However, if I have multiple calls to get_json_response(), it gets messy to include that same closure over and over.

My solutions is to change it to:

use serde_json::Value;
use std::error::Error;
use ureq;

fn main() {
    let json =
        get_json_response(format!("{}{}", BASE_URL, LOGIN_URL).as_str()).unwrap_or_else(fail);
}

fn fail(err: Box<dyn Error>) -> ! {
    eprintln!("Error: failed to get: {}", err);
    std::process::exit(1);
}

This doesn't work because unwrap_or_else() expects a Value to be returned rather than nothing !. I can cheat and change the return value of fail() to -> Value and add Value::Null after the exit(1). It works but feels wrong (and complains).

I could also do unwrap_or_else(|e| fail(e)) which isn't terrible.

Is there an idiomatic way to handle this?

marcantonio
  • 958
  • 10
  • 24
  • @user4815162342 Oops, you're right, I got it backwards. `.expect()` should do it. – Herohtar Sep 03 '21 at 21:45
  • @Herohtar `expect()` will panic if the result is `None`, whereas the OP wants to control the way the process exits. – user4815162342 Sep 03 '21 at 22:01
  • @user4815162342 Do they though? They didn't say they didn't want to panic, though I suppose it could be assumed. However, it is recommended to panic in most cases, according to [this answer](https://stackoverflow.com/a/39229209/574531). – Herohtar Sep 03 '21 at 22:04
  • @Herohtar I can't speak for the OP, but existing code that uses `unwrap_or_else` specifically to avoid panic is a strong hint. The answer that recommends always panicking neglects that you sometimes want better control over the error message and exit status, which I expect is the case here. – user4815162342 Sep 04 '21 at 07:06
  • While `!` can be coerced to any `T`, `fn()->!` cannot be coerced to `fn()->T`. This is why the lambda works but the function doesn't. – kmdreko Sep 04 '21 at 15:20
  • @kmdreko I suppose this is because the never type still hasn't really made it to stable Rust? – user4815162342 Sep 04 '21 at 16:51
  • @user4815162342 its not just `!`, `fn()->Box` cannot be [coerced](https://doc.rust-lang.org/reference/type-coercions.html#coercion-types) to `fn()->Box`. Functions can only be coerced if its a subtype, which only covers things like: `fn()->impl Supertrait` to `fn()->impl Trait` or `fn()->&'static T` to `fn()->&'a T`. Coercion != variance. Stabilization of `!` might end up addressing this, but I'm not familiar with that effort. – kmdreko Sep 04 '21 at 18:08
  • @kmdreko *Functions can only be coerced if its a subtype* - I gues I'd expect any type to be a subtype of `!`, but the feature in nightly [doesn't appear](https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=6b77e154116b4593c41beb3f8670513c) to work like that. – user4815162342 Sep 04 '21 at 19:00

2 Answers2

3

As pointed out by @kmdreko, your code fails to compile because, while ! can be coerced to any T, fn() -> ! cannot be coerced to fn() -> T.

To work around the above, you can declare fail() to return Value, and actually return the "value" of std::process::exit(1). Omitting the semicolon coerces the ! to Value, and you don't have to cheat with a Value::Null:

fn main() {
    let _json = get_json_response("...").unwrap_or_else(fail).as_str();
}

fn fail(err: Box<dyn Error>) -> Value {
    eprintln!("Error: failed to get: {}", err);
    std::process::exit(1)
}

Playground

user4815162342
  • 141,790
  • 18
  • 296
  • 355
  • 2
    Furthermore, if there are different `Value`s with the same logic, `fail` can be made generic - https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=6868e6d5133b97ae37c69429e4e90409. – Cerberus Sep 04 '21 at 04:01
0

I'm not sure if that is idiomatic to Rust, but you can put all the stuff inside a closure and handle the Result afterwards:

use serde_json::Value;
use std::error::Error;
use ureq;

fn get_json_response(url: &str) -> Result<Value, Box<dyn Error>> {
    (|| { 
        let resp = ureq::get(url)
            .set("Authorization", format!("Bearer {}", API_TOKEN).as_str())
            .call()?
            .into_json()?;
        Ok(resp)
    })().or_else(|err: Box<dyn Error>| {
        eprintln!("Error in get_json_response(): failed to get: {}", err);
        std::process::exit(1);
        // or just return Err(err) if you don't want to exit
    })    
}

fn main() {
    let json =
        get_json_response(format!("{}{}", BASE_URL, LOGIN_URL).as_str()).unwrap();
}
Dmitry Mottl
  • 842
  • 10
  • 17