I've also thought about this, and I've changed preference from checked + unchecked exceptions (like Java) to a built-in ok|err enum plus panics (like Rust).
There are several reasons for preferring returning return/err type:
- Type system should be used as much as possible to check things statically and avoid conflicts between returns from different places. Reusing common exceptions is bad, using integer/boolean status code is terrible.
- The information about the problem should all be together. Don't just say 'it failed' and let the caller retrieve the result information.
- For problems the caller may have to fix, it should be clear from the signature that they can happen.
- It should be clear reading the body of a function where the exit points are (at least for recoverable/reasonable execution).
- But it must be easy to propagate the error to a higher level (e.g. ! syntactic sugar, which is a reason to have ok|err built in).
- There's no need to have multiple systems for getting data out of a function (exceptions/returns), one is enough.
- Checked exceptions have been found to create too much interface pollution and not scale well.
- It's also convenient to treat problems at different levels of abstraction. E.g. sometimes you want to know exactly what went wrong, something only that it's an IO problem, and sometimes just that it failed.
- It should be hard to accidentally continue in broken state after a problem. This is an argument in favour of exceptions. For return values, the language (or linter) should suppose some kind of must-use flag on the result.
I'm a little sad to even mention the first two points, but there is still a lot of code that uses true/false to indicate success/failure and makes the caller get the information about reason etc.
Note that I'm focusing on the return value, but once it becomes apparent that a problem isn't recoverable, panic (or runtime exception) is a good solution and some of the above considerations no longer apply.
I feel that irrecoverable situations are either 1) out of control, so no need to design around it, or 2) things that are bugs if they happen, so write fixes instead of error handling.
EDIT: note that this is possible, but not very convenient, in many languages. The language should have sum types (not crippled enums that only work with primitives), a shortcut for propagating error return values, and a way to warn/error if a return value is not used.