> the only (sane) way I can think of handling this is propagating the error down to the callerA big problem, among many, with doing that is that you leak implementation details out of the abstraction. If, to stick with your example, you have a function that helps you with reading files, the caller shouldn't care where the data is stored. Today it might be the local filesystem, tomorrow S3, and when you make that change nothing about the rest of the program should break.
But you can't count on the lower level functions using the same errors. As you suggest, a "a file I’m trying to read doesn’t exist" isn't represented as a "bucket 404", even though at a higher level they are the exact same thing. If you straight returned the "a file I’m trying to read doesn’t exist" error as you got it from the file API, now the caller is going to depend on that, and when you replace it with the S3 function that returns a "bucket 404" error, everything starts to break.
What you typically want to do is return a more generalized "not found" error that can remain stable regardless of specific implementation details. There are rare cases where you can get away with simply returning the value up the stack, but in the majority of cases you need to handle the error, either by doing something with it or returning a new error that is more useful to the caller. And, so, try becomes essentially unusable without the language taking a larger macro take on supporting such a feature.
Like the sibling comment points out, Rust does "from" conversion when using try (?) to try and avoid encountering the same fate.