I think that Rust's approach is very good and superior to everything else I know. It's even something which can be implemented into other languages.
Basically, there is no throw or additional channel, which communicates errors. Instead, when something can go wrong, you simply return a Result<T, E> object, which has to be handled in order to process the result. What you can now do is either handle the error and try another call, or have the error bubble up through all function calls, until it hits an API or user-facing code, which then can automatically handle the exception, or display it. The advantage the regular function result is used to communicate the whole result of a call (not only the success one), and it enforces proper error handling without requiring developers to read the docs to find out that your function actually might throw an exception.
In Rust, there are additional libs, which add error-chaining, scoped error handling, etc. to the mix, but all that stuff build on top of the core mechanic of Results, which are used throughout the std lib :)
Another piece of the puzzle to better error management is to get rid of error whenever possible. For example by using optional values. In all cases, where there might be a value, or might not be a value, you just say, that there is an object, which stores the state of something being there or not, and if there is something, also stores the value. That way devs are forced to handle the case when there is nothing. Not doing so is more difficult than just hoping that there is no null . Again, Rust has a stdlib struct for that: Option<T> .