After thinking this over, my inclination is to go to “both extremes” (I wouldn’t call it a “middle ground”).
1. Provide blocks.ParseError, blocks.DownloadError, etc. Individual functions use these, making it feasible to perform fine-grained error handling by matching on the type — as advocated in the article.
2. Provide a library-wide blocks.BlocksError non-exhaustive enum which is one layer of abstraction above the others and implements From for each of them. Define blocks.Result using this blocks.BlocksError. This allows users to write a function invoking library code which returns a blocks.Result, supporting the propagation use case.
It doesn’t seem like adding blocks.BlocksError on top of the others is that much more work. The hard part is creating the localized modular error types.
It's writing all the ParseError, DownloadError, etc that I'm suggesting you might want to avoid in certain cases
Unfortunately due to the fundamental design of Rust error handling eyre & anyhow capture backtrace information at the moment their type is created. So the lower down you use it the better error messages you'll get. And "ParseError on line 134 in file foo.rs" can be much more valuable than "FooError on line 5 in file foo.rs: Caused by ParseError".
1. Provide blocks.ParseError, blocks.DownloadError, etc. Individual functions use these, making it feasible to perform fine-grained error handling by matching on the type — as advocated in the article.
2. Provide a library-wide blocks.BlocksError non-exhaustive enum which is one layer of abstraction above the others and implements From for each of them. Define blocks.Result using this blocks.BlocksError. This allows users to write a function invoking library code which returns a blocks.Result, supporting the propagation use case.
It doesn’t seem like adding blocks.BlocksError on top of the others is that much more work. The hard part is creating the localized modular error types.