[API design] Returning dependency errors

Imagine that you pull in crate foo as a dependency. It has a foo::Error for its own errors, so you set up a From conversion to do implicit conversion from foo::Error to your own library/application error.

At some point you call foo::Foo::do_something(), and the implicit conversion doesn't work. It turns out that foo uses bar as a dependency, and Foo::do_something() is a very thin wrapper around some function in bar, and for whatever reason foo's developer chose to have the wrapper be so thin that it returns bar::Error.

I have encountered this in the wild, so it's not hypothetical -- but I'd say its rare (as far as the crates/functions I have used). Personally I tend to wrap other crate's Error's in my own errors, because I don't want end users to have to pull in separate crates just to get access to Error types.

There is a sort of a middle-ground, I guess: Re-export the dependencies, so the developer doesn't need to explicitly import them in their own Cargo.toml.

Where on the transparent-to-opaque scale do we want sub-dependencies to, generally speaking, be in the ecosystem?

Back-story: I'm in a situation where I set out to remove some in-crate objects and use dependencies instead. I am pleased with the result, except for one particular place where I expose a foreign Error. I can work around this by adding an error-conversion wrapper -- but this is the sort of thing I set out to clean up. So it got me questioning why I put so much effort into hiding sub-dependencies?

I have always treated dependencies opaquely, even back in the C/C++ days -- it never really occurred to me to not do it in Rust. But apart from "following tradition" and "being idiomatic", are there technical reasons why crates tend to be opaque? What manner of can-of-worms would one be opening up if one would start to lean on the more transparent side of things?

If you expose a dependency's type, then the specific version of that dependency becomes part of your public interface. That may be fine, if it is a dependency that all involved parties needs to agree on anyway, but if there is no other reason why your dependent needs to know about your dependency (as in this example of a single error type), then it's an additional constraint that may interfere with your ability to upgrade your dependencies (or replace them with different libraries entirely) without breaking your dependents.

2 Likes

I should also add: if you reexport a dependency (call it A) and your dependent (call it B) is careful to use that reexport when interacting with your use of A's items, then it's not a breaking change to change the dependency on A. But that's a more delicate situation, particularly if B is using A for its own purposes too, in which case they will not get any compiler/IDE support for using the right names for each and not assuming they're the same.

So, not exposing any dependencies' items is a much more robust situation to be in.

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.