They still can't tell you the call stack, you still need to propagate them manually, they are still very easy to accidentally ignore (especially on functions which don't return anything else), they still add huge amounts of boilerplate, making it hard to read the code and distinguish <happy-case logic> from <regular error-bubbling logic> from <the places where errors are actually handled>.
It's a different way of programming, but a more explicit one, and I don't think implicit Bad Thing™ should be easy in a language.
(By the way, Rust has a single-character operator to emulate exception pass-through.)
Sounds like exceptions.
This forces you to think: what should I do with this Result error code? Should I propagate it or handle it? This thinking is good and produces good design and good error handling.
For beginners it is very useful, that, for instance, any function doing I/O does return a Result: it forces you to think how your system should behave in case of I/O error.
In C++ or Python, exception control flow is implicit. All functions could theoretically partecipate in an error chain and you have no idea what, if and why a function will raise an exception. Functions doing I/O are completely indistinguishable from the purest functions, and only experience tells you where and if errors should be handled. The consequence is that it’s very hard to find mature code based with sensible error handling.
I should notice that Go is basically the same here: the main difference is that it’s more verbose in error propagation, and requires a configured environment (linter errcheck) to detect when functions only returning err are called without error handling/propagation. Rust is superior here, but Go has the same basic architecture, just less evolved.
Forcing a global (on thread or coroutine level) handler is better, as actually doing that will mean you have to structure the application in a particular way to handle anything, and that structure makes error handling actually easier.
Just make it fail to compile if the correct handler is not being installed and ensure every error is identifiable.
This is how hardware works anyway internally - interrupt vectors. Having a library to help with correct return and handling of error state is enough. Allowing return only to place that caused the error is saner than what C++ and Java do. There would be no temptation to pass the buck and you would have to model error state to be fully visible, enforcing it to be debuggable and part of explicit API.
You could consider it syntactic salt much like Rust's memory management is.
Error codes are like someone suggesting "lets blow this thing up" and you can either follow their advice or ignore it.
Exceptions are more like someone throwing you an unlocked grenade. Unless you expect them to be that irresponsible and unless you actively react to their behavior, things explode.
I once had to debug an issue with a Java app that did not throw exceptions anywhere, but it did use the Hibernate ERM framework and somewhere deep inside Hibernate there was a bytecode patching library used which didn't like our custom build of the VM and produce a class-less unchecked exception which would crash the app.
And in my experience, the people that do bad error handling with return codes are also the same people that catch an Exception and rethrow it wrapped in a RuntimeException to remove all that try/catch bloat. It's laziness in either case.