The idea of structured programming was that control flow should be constrained in predictable ways. That free form flow made programs unreadable, and impossible to extend. The term spaghetti code was coined to describe the complete tangling of control flow that unstructed programs led too.
That was in the days where there was no if/else statements, no procedures, no functions, no classes, no case/switch, no for loops, etc.
Those languages are few and old, assembly languages are like that, and some early versions of Basic, Cobol and Fortran were like that. These represent the essence of the imperative style.
All control flow was thus handled with a JUMP instruction, commonly known as GOTO.
Then came Dijkstra and his famous Goto considered harmful paper. Which started the debate into unstructured control flow, i.e., being able to jump anywhere from anywhere, being bad, and that we shouldn’t allow that anymore, that programming languages should restrict the possible set of control flows. Then came Bohm-Jacopini’s theorem (actually came before, but got popular later), which proved that you can implement all programs without goto, if instead you are given three structured flow controls: procedures, if/else, and while loop.
Forward 50 years and here we are, discussing structured programming.
Error handling used to be handled with GOTO’s also. When procedures were added, that is, a structured jump into a sub-program, where all values are pushed on a stack, and when execution is over, it jumps back to where we were and pops the stack. What we most often think of as a function or method today, and the callstack. There needed a way to handle errors in sub-programs, aka procedures, in what was the essence of the procedural style.
The early strategy was to return an error code, and use an if/else conditional to fork the flow from the caller’s side. Some people found that inconvenient, too verbose, too easy to forget to handle the error code, and too hard to figure out what error codes could be returned. Exceptions came along to solve these issues. Java tried to solve all three problems, but most people found that declaring thrown exceptions was still too annoying and verbose. So most people found that simply having implicit error handling, and implicit error return types is enough. So exceptions default to if error rethrow, and all procedures return Either undeclared exception or declared result.
Now comes along functional programming. The idea is that the data accessible to sub-programs should itself be restricted to only what is explicitly pushed to the subprogram stack. Thus your procedure should only be allowed to read and modify its input parameters. This is the lambda calculus fundamental rule of computation by substitution.
In that sense, exceptions are functional if they follow the rule of only using the input parameters.
What typed languages like Java, Haskell, Scala don’t like about them in my perspective has nothing to do with the functional style. Its just the same old debate. They don’t like the implicit error handling, and they especially hate the undeclared nature of the returned exceptions.
So Haskell said, I want to force you to declare the errors you can return explicitly. Just as Java did with typed exceptions. Haskell then said, I want to force you to explicitly handle the returned error. But, to make it easier, I’ll give you a monad that can easily compose (i.e., sequence) error returning functions together without needing you to explicitly insert the error handling code between each. Throw some pattern matching and some syntax sugar, and maybe its not too verbose anymore.
In a dynamic world like Clojure, declaring the error type is an irrelevant question. It was already decided not to declare any type. Thus all return types are implicit, including errors.
So the final question is, should errors default to an implicit handling? The alternative being you just let it crash right here and there. I think most people would agree its best to bubble it up by default, in case it can be handled higher up the chain. Haskell avoids this by not compiling. So defaults are unnecessary, as it makes sure you always explicitly chose an action.
But the real discussion point for Clojure I believe is more related to the inability to use normal functions and macros to handle an exception. To put it differently, you can only bypass the default by using try/catch. That can be annoying when you do want to handle the error in another way. Which is normally when people are going to look for an alternative, such as returning error codes, as a way to bypass the default handling.
No real answers here, but all problem solving starts with better understanding of the problem, so I hope I managed to do that at least.