What to do when an Error contains a borrowed value?

Hi all,

This came up in How to return a nom Parser<_, _, _> from a function? but I think it's a sufficiently general situation that merits its own thread.

I'm currently learning Rust and I've noticed a situation coming up a few times, which is when a function that I am calling returns a Result<..., err> and the err type contains a borrowed value nested somewhere within it.

In the linked example, I have something like this

    let input = std::fs::read_to_string("data/input.2")?;
    mk_parser().parse(&input)

where

pub fn mk_parser<'a>() -> impl Parser<&'a str, MyThing, nom::error::Error<&'a str>>

and calling .parse(&input) on this Parser returns me an

pub type IResult<I, O, E = error::Error<I>> = Result<(I, O), Err<E>>;

Most importantly, note that the E in this error type contains a nested &str which is borrowed, ultimately, from the input: String defined on the first line. So we cannot return this error from this function.

The only way I can figure out how to turn the error into an owned value, which I can then Try with the ? syntax, is to pretty much render the entire error to a String.

        .map_err(|e| e.to_string())?;

But that seems like a huge hack... surely there must be something better than this? Am I holding it wrong or is there something much better that I can be doing here?

This whole topic about "should the return type be a complex domain specific ADT or a simple string" comes up in every language and the answer is usually to map middleware errors into your app's relevant error containers (which almost always end up being roughly "retryable", "fatal" and "can be fixed by interacting with a user"). But Rust brings this extra complication where the error type might not be allowed to escape a particular scope because of ownership rules, in which case it forces us to do any handling of that domain specific error type at this layer which is very restrictive because often times we may want to factor that handling into its own module. Imagine if main were actually a function that took the filename as input, it would mean that we MUST handle the domain specific error (either mapping it into our own ADT or rendering it) here because we can't return it as it is. And that must be done in every function that hits the same problem, instead of being able to do it in a catch-all error handler as is common in other languages (most notably that's very common in Haskell and FP Scala, but I've even seen it done in Java and golang).

There is a nom specific way of turning the &str into a String and make it owned by calling mk_parser().parse(&input).map_err(|e| e.to_owned())?; ... but this might not be available in the general case where some other library returns a borrowed value in its error.

What's an idiomatic way to deal with this when it happens? I feel like perhaps it would be good practice for crates to only return owned values in the error type to avoid restricting the caller in these cases, but that won't always be possible for performance reasons and whatnot which is what I assume nom has gone for.

I don't want to get into the situation that for every crate I end up having to create copy/paste ADTs of their error types with owned values in them, and writing convertors. That'd allow the app error handling to still be done centrally but it is excessive and very brittle against upgrades in crates.

Nope. That's what you have to do. Convert the error type into something that isn't borrowed.

I don't think there's anything else sane you can do about it.

5 Likes

But… why?

1 Like

because I've just flattened a domain specific ADT down into a String and lost all the contextual information that I might have wanted access to higher up in my error handling logic.

1 Like

Well I thought we were talking about converting the &str in a nom::Error into a String? For that, str::to_owned()/to_string()/into() is the right way; the outer structure remains intact.

1 Like

Yes, it's correct for nom because that is available, but I'm not sure if that's available in the general case (which is why I split this off as a general question rather than a nom specific one). From my understanding, use .as_owned() if it is available, otherwise you're stuck with .as_string() (which I would hope is always available!).

In this case Error is just a wrapper over a (public) &str (the generic I in its definition) and a (public) ErrorKind. You can just recreate an instance of Error converting only the &str to the owned version and you will lose no information.

1 Like

Yeah, formatting into string is what I usually do. You can introduce more structure if you're actually consuming it to try and do some sort of error recovery.

It's conceivable you'll run into some library with an error type that isn't 'static with no way to losslessly convert the error to something owned, but that would be a poor library design.

2 Likes