First, you'd use match statements instead of if-then so the worst case scenario already looks much better than in C, especially since Rust has enum decomposition and makes expressions first class (you can do
let y = if (...) { y } else { z }). There are a lot of ways of combining control flow and assignment operators to decompose union types like Option and Result (which is Option with error E instead of None). For example, you can do
if let,
while let, or
let x = match(y) { ... } that allow you to handle errors without jumping through hoops like you would with C returns or C++ exceptions. For a library consumer this covers a large number of cases.
Second, most of the time you can use the ? operator to greatly simplify error handling. Any code path that shares the same error type can just pass through errors as if they were an exception. The ? operator is also aware of Into/From trait implementations so in many (or even majority of) cases you can use the ? operator even if the function you're calling has a different error type! If I remember correctly, you can even exploit the module system to provide a per-file Into/From implementation for your custom error type if it doesn't map cleanly across use cases (they just have to be in the same module as the type). Rust's zero cost abstractions and trait system make it really easy to reduce boilerplate without compromising on readibility or discoverability.
Third, the Result type has a number of functional operators for responding to and combining results. You can do Result.and_then(...) with a closure or function call to do something when there is no error and Result.or_else(...) to do something when there is an error. You can use the and or or operators to combine multiple results and there are many other utility functions that are universal to Rust error handling that make the entire experience very pleasant, composable, and readable with a little experience.