Is error-throwing bad functional programming?

My introduction to functional programming came through Clojure. Reading through the book Functional Programming in Javascript recently, I was surprised when it said quite reasonably that try-catch style error-throwing is bad functional programming because it constitutes another exit-path from a function and breaks function-piping and therefore functional programs. I was surprised because Clojure seems fine with try-catch throwing and I’ve even started to use ex-info throws rather heavily in my programs. So what do you think – is there a better way than error-throwing?

9 Likes

I may be going off-topic, but I’m uncomfortable with this question on several levels:

First, there is no, formal, unanimous definition of Functional Programming - it means a lot of different things to a lot of people, which means that any argument supporting “X is good / bad Functional Programming” is somewhat built on sand. I prefer to think of it as a mental model informing / enlightening the way we do software engineering than a set of rules to abide by.

Second, I question the usefulness of “is X good Functional Programming?” questions for pragmatic software engineering. This question seems to be a matter of aesthetics or style rather than effectiveness - a bit like “is AC/DC really Heavy Metal?” or “is it still Jazz if I’m using a Pentatonic Scale?”. When programming, the reason we should ask ourselves “should I use language feature X / architecture Y?” is not to be in harmony with some paradigm like Functional Programming, rather to meet some goals of clarity / ease of reasoning / safety / etc.

Third, the argument you mention against exception throwing/handling applies to Imperative Programming / Structured Programming as well - it breaks sequential execution and flow control branches just like it breaks function piping.

About which is the “better way”: in principle, I’m personally fine with exception throwing/handling, but my experience has only been in languages that have it, so my opinion is not the most valuable in this matter. In practice, I believe that (as usual) the answer is that it’s a tradeoff, having to do with whether or not you have a holistic approach to program execution. I think languages like Haskell, with their all-encompassing type systems, make it more practical to have a holistic approach, whereas a hosted, dynamic language like Clojure encourages a more “open-world” approach.

15 Likes

Excellent answer by @vvvvalvalval. To add to what he said, a few years ago I went down the path of trying to eliminate exceptions from my Clojure programs. Check Adam Bard’s excellent blog for some examples of how to do that.

My personal take-away is that, unless you have specific requirements, explicit error handling, i.e. returning failures as data, is often not worth the effort. This is true especially in web programming where a common pattern is to let the handler function (or middleware) intercept exceptions and return the appropriate error message to the user. In my experience, this pattern works well, keeps the code clean, is encouraged by the host (the JVM) and is sufficiently flexible. Whether or not this pragmatic style of error handling qualifies as Functional Programming by any definition is of little importance.

12 Likes

What a beautifully Clojure answer :slight_smile: Thanks for explaining the alternative error-handling methods.

EDIT You’re right; that’s an excellent blog post on error handling

3 Likes

If you still want to eliminate exceptions and manage them the Haskell way you could use the cats library that brings many Haskell concepts into Clojure.

1 Like

Very interesting! I don’t actually know what the Haskell way is, but I’ll take a look

There are usually two kinds of runtime errors - ones that you can recover from, and others that you cannot recover from. Irrespective of whether or not you can recover from an error, the path from error-discovery to error-reporting is what determines what degree of flexibility you may need to handle the error. In a web application, when you cannot connect to the database you can either simply return HTTP 500 or (for example) fallback to reading stale data from from a cache. Similarly, when you have an input error you can either straightaway return HTTP 400 or (for example) try to find typos in the input based on Levenshtein distance and try to construct a more helpful error message.

For most simple cases, where it is straightforward to turn discovered error into error message, you can probably use a generic handler (probably as a Ring middleware) to translate exceptions into error messages. However, when error-discovery leads to several post-discovery steps you probably need more powerful error handling. In such scenarios you may need to express error as data in order to have flexibility and composability during error processing.

Whether exceptions are enough or you need to express error as data really depends on your taste and your needs. In my experience, complex error handling with exceptions has led to unwieldy pattern matching and imperative code. On the other hand, expressing error as data has let me operate at a higher level of understanding about how to handle various scenarios.

Full disclosure: I’m the author of Promenade; my bias may be obvious. :slight_smile:

7 Likes

I was hoping @kumarshantanu would chime in since I knew he had a lib about this specific problem. Glad you did :+1:

1 Like

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.

18 Likes

Excellent and instructive response, illustrating Haskell’s (and
that javascript book’s) approach as well as others. Thank you.

@Webdev_Tory you may like to try https://github.com/adambard/failjure for a light-weight monadic approach to error handling in Clojure.

2 Likes

Looking through it and related links, it would be great to have a written justification of the Monad approach. I’d love to see some code examples where Monads make life better.

Exception-throwing functions don’t compose, and that’s why they’re considered “bad” in many FP circles. I’d argue that composition is one of the primary pillars of programming (alongside abstraction), so you really don’t want to break composition if you can avoid it.

Fortunately, Monads have been around forever to model failing computations as sum types (Maybe monad with Some or None, Either monad with Right or Left) in a way that is “composable” (monadically composable).

Even though reasoning about monadic composition is not as straight forward as pure function composition, it’s a thousand times better than having non-linear jumps inside your call stack. It’s the equivalent to the mail delivery service leaving a letter in your mailbox vs having the mail deliverer reify from thin air anywhere in your house and whisper the letters to your ear at any time of the day, unpredictably.

Can you give an example? In my perspective, exceptions are linear jumps, so I’m not understanding your argument. The exception throwing function returns to the calling function with an exception. It properly unrolls the stack, linearly from its natural push and pop ordering.

The argument I normally hear is related not to order, but its the fact that exception handling constructs operate like macros. Thus you can’t use normal functions or existing control flow to operate on a thrown exception.

That is, exceptions are implicitly handled for you, and if you don’t like it, only the try/catch block can be used to override the behavior.

That said, even Haskell recognises that for faults, i.e., unpredictable exceptions, this implicit behavior makes sense, and an implicit bubbling up mechanism is needed, otherwise we’d have to add fault handling logic manually to every line of code.

For known exceptions, the jury is still up for debate. If you returned it as normal, every function type with known exceptions become a sum of exception + normal return type. And as the user of such a function, you’re now forced to handle both cases, even if you know that the way you use the function can not return the exception. Or, even if you know you can only recover from higher up the stack, thus don’t really care at the level you’re at, but just want to bubble it up.

Java tried something fancy, with checked exceptions. Where you double type functions with the return type, and a list of all exception types it can return. And the compiler makes sure all calling functions handle them, but gave you a way to annotate if you want implicit behavior. Most people just found it to be a drag of extra verbosity and little utility.

In Haskell, I think it’s not as bad, because everything is more pure, and pure functions rarely throw known exceptions. Thus overall, there’s less places you’re forced to handle exceptions. Normally they just have a proper behavior on all their input values. You can’t give them garbage either, because of the strong type guarantees.

Now in Clojure. I find things are more interesting. You can always decide to return errors instead of throwing them. The callers don’t have to handle those either, since it’s all untyped. Nil is often used as the “you gave me garbage and there’s no logical output for your input so here is nil”. Every Clojure function can receive garbage in, since there’s no type checker to prevent it from happening. This garbage could also result in unpredictable exceptions, which can thus happen more often. Now in that case, I feel an implicit default handling makes sense. I still wish Clojure wasn’t limited to try/catch, and could have more powerful constructs instead, but with macros, its possible to create some yourself.

1 Like

What I meant by non-linear: any point in the stack might catch (and swallow, or change, or rethrow) any exception you throw, which might make the callers up the stack catch a different exception (if it was changed), and in turn swallow, or change, or rethrow it), which to me it feels like an impossible thing to reason about.

In the sum type world, if you only want to talk about the “happy path”, you just use fmap. If you care about the other cases, you may choose to pattern match and/or use the either equivalent of mapping Left values in your language. It’s up to you, and you can always just ignore (bubble up) the bad values by just using the very optimistic fmap. Same, yet still just values, no strange manipulation of the stack.

Reading this post I don’t see an agreement about what should be the right approach for error handling in clojure, what uses most of libraries on the ecosystem?

Most functions return nil for invalid input, or use exceptions to indicate unexpected or transient errors. Monads are popular in Haskell and OCaml circles but are not much used in dynamically typed languages like Clojure.

3 Likes

I’d love your take on why this is, @pesterhazy. Personally, I expect it’s related to the concept of nil-punning and the fact that, ultimately, error throwing is a sort of typing with all the benefits (hail Haskellers) and drawbacks (hail dynamic-language folks) that go along with it. What do you think?

Sounds right to me. Static typing helps you catch errors, which justifies the additional cognitive burden associated with managing and unwrapping Maybes, Eithers etc.

Like @pesterhazy said. Just rely on throwing normal exceptions using throw. Or return nil if it makes sense in the given use case.

When you throw exceptions, you can use ex-info and ex-data.

Example:

(defn i-throw [a b]
  (throw (ex-info "I always throw." {:type :always-throwing-ex :args [a b]})))

(try
  (i-throw 10 20)
  (catch Exception e
    (when (:type :always-throwing-ex (ex-data e))
      (prn e))))