The proper way to use exceptions is when you want behavior that bubbles up. So if you do x=f(g(h(x))) and h(x) fails because the user didn't configure something, you only need the config-checking logic in h, not in g or f. This use-case is quite rare though, and overall not worth the extra language complexity.
I think there is one underused error handling mechanism: "asynchronous error handling" where you have to to ask for errors explicitly. Like in the OpenGL API, for example. It might be the best choice where there is a lot of communication between largely isolates states.
So that could be one answer: If possible design the API in such a way that functions that can have expected errors [1] do not return values that only make sense if there was no error. Because this means that control flow gets very irregular on the calling side. Such a design could be hard to do though, I don't know.
The other answer could be: Don't have errors that you don't know how to handle. Unless you write very low-level code, like a library (which means you probably want to delegate the handling), just die on errors that you don't know how to handle. Make the die() call a best-effort cleanup routine if possible, and keep the important to-be-cleaned up state as global as possible (NOT on the call stack), to support such a cleanup routine.
[1] I hate the vagueness of that term, but take it to mean "no statically obvious wrong usage by the caller", like passing in made-up or corrupted values
Often yes (one of the exceptions being a simple die() routine that cleans up only global state). It's just pseudocode, and in real code you will often not call any functions in the exception handling block.
open filepath `catch` handle_file_not_found >>= use_file_handle
handle_file_not_found :: FileNotFoundError -> IO a
use_file_handle :: FileHandle -> IO a
The drawback is "properly-RAII-ed code". It's hard to write because where you write the RAIIed classes you don't have the context of the place / places where it's used. It's extremely hard to read and step through with a debugger - because control flow is all over the place. I don't want to write classes instead of simple regular control flow.
And regarding standard control flow code, I've never seen a code base that handles exceptions correctly / elegantly / in a disciplined way. The only way to deal with exceptions is handling them immediately, i.e.
To be honest I prefer C-style return code handling. It would be only 2 or 3 lines instead of these 4, and no additional indentation, and no additional value return mechanism. The only cost of C-style handling vs exceptions is duplicating a few library functions: Need to offer interfaces that die and ones that allow handling the error. And there's the possibility of forgetting to handle the error (if the caller considers it an error, after all) once in a long while. But you have that problem with exceptions, too...In other words, exceptions are good for writing sloppy code (I like writing Python scripts, but I rarely handle exceptions), and very bad for serious code bases.