rusterror-handlingcloneborrow-checkerownership

How to avoid a clone when passing a value to a consuming function, but needing it back in the event of an error?


Say I have a function that calls a Rust API that consumes a value. That API function returns a Result, potentially indicating an error. If the error happens, I need some information from the original value to pass back up to this function's caller.

use thiserror::Error;

#[derive(Debug, Error)]
enum InnerError {
    #[error("Inner error occurred")]
    SomeInnerError,
}

#[derive(Debug, Error)]
enum OuterError {
    #[error("Outer error occurred")]
    SomeOuterError(u32),
}

//#[derive(Clone)]   What if this isn't possible?
struct MyStruct {
    // arbitrarily complex, assume not clonable
    x: u32,
}


fn foo(v: MyStruct) -> Result<(), InnerError> {
    // Consumes v
    drop(v);
    Err(InnerError::SomeInnerError)
}

fn bar() -> Result<(), OuterError> {

    let value = MyStruct { x: 42 };

    // If this fails, I need info from the original value to pass back to MY caller:

    foo(value)  // .clone() not possible
        .map_err(|_| OuterError::SomeOuterError(value.x))
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    
    bar()?;
    
    Ok(())
}

Rust Playground

For the sake of this question, please assume that foo(v: MyStruct) is fixed and that I am not able to change it - it consumes the value and that can't be changed (is this a good API? I don't know... maybe the answer to this question will answer this too?).

This won't compile because in foo() the moved value is then illegally used inside the .map_err() closure. If value: MyStruct was able to be cloned, then this would be less of a problem, because value could be cloned pre-emptively before calling foo(value).

But in most cases, if the error does not occur, this clone is a waste of time. Perhaps the compiler would optimise it away? Does anyone know for sure that this would happen?

BUT if the clone isn't possible (due to whatever MyStruct might actually hold), then how would the original value be retained for the error mapping?

I can imagine that foo() could pass back ownership of value in the error variant, such that:

enum InnerError {
    SomeInnerError(MyStruct)
}

This would allow the error mapping to extract it from the error rather than the local scope:

    foo(value)
        .map_err(|e| OuterError::SomeOuterError(e.x))   // get the info from e not value
}

Unfortunately, if InnerError has many variants, a cumbersome match would be needed in the map_err(...) closure to pull out the original value from each one.

    foo(value)
        .map_err(|e| {
            match e {
                InnerError::SomeInnerError(inner_value) => OuterError::SomeOuterError(inner_value.x),
                InnerError::SomeOtherInnerError(inner_value) => OuterError::SomeOuterError(inner_value.x),
                InnerError::AnotherInnerError(inner_value) => OuterError::SomeOuterError(inner_value.x),
                InnerError::NotTheLastInnerError(inner_value) => OuterError::SomeOuterError(inner_value.x),
                // you get the idea...
            }
        }
}

The data needed for OuterError could be individually picked out of value with clones on all pertinent fields, but that seems a bit ugly, and impossible if MyStruct has private fields and I need to call a method on it instead.

I imagine for cloneable structs, most people just accept the extra .clone() and move on, but if it's not cloneable, then what?

Is there an idiomatic way to handle this situation?

Should fallible API functions like foo() never consume, but always take a reference as a rule?


Solution

  • Quick Answer

    A very good example of this is mpsc::sender::send. This is venerable code that follows the solution of returning the input back. This is probably good to replicate in most cases where a possibly erroring function consumes.

    Long Answer

    Earlier on you specify that the fn(MyStruct) contract can't change. In this case there's not way to recover the value after the function is called, even on error. The value will be dropped at the end of that function unless returned or moved elsewhere. The best you can do without cloning is to copy/clone out the fields that you can and need.

    If you can change the function then this gets into a few questions about your function design: "Do you need to consume?" and "What do you return?"

    Do you need to consume?

    Consumption is a strong requirement. This means that either the function is moving the value or dropping the value. In the case of moving the value (ex. mpsc::sender::send) the function can usually move the value back to the return when erroring. In the case of dropping (ex. dropping a MutexGuard for some reason) this means you have a clear boundary where you can send the value back.

    Consumption is a rare requirement outside of data structure functions that move the value, those also tend to return the value back on (immediate) error in the standard library.

    What do you return?

    As I mentioned above consumption is rare outside of data structure functions, but those functions tend to return the value back to the caller if they fail. What should the return type look like then?

    You have a few options:

    Make a Struct/Tuple

    If the value can always be returned (the value can be moved out of the data structure or moving the value into the data structure means success) then you can simply make a tuple return fn(MyValue) -> Result<(), (MyValue, MyError)> or build a struct for it:

    struct MyErrorAndValue<T> {
        value: T,
        error: MyError,
    }
    

    If the value can't always be returned you can make the MyValue into a Option<MyValue> return.

    Add it to your enum

    The most "correct" error type would be to only include the MyValue return with the error if that error variant can occur with a returned value. This does run into the issue you outlined though so I wouldn't recommend it.

    Don't

    If you're making a library what if you just didn't return value back? This may not be the best API design but you may also just not want to spend the time to support it or even think about it when there are harder more interesting problems to solve. A finished good project is better than an incomplete perfect one.

    Conclusion

    There are quite a few ways of looking at this problem. In my experience it's extremely rare to find a value that you can't clone, need to consume, and can't just put in an Arc (don't do this by default, only in cases where the object is large and you don't need to mutate). Taking a queue from the standard library is a good idea though.