Is error-throwing bad functional programming?

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))))

Elixir is dynamic and has a pretty different approach to this, clojure can learn about it

We’ve discussed this before here Error handling in clojure

In that thread, we weren’t able to actually identify what problems Elixir solved in its handling of exception over Clojure’s. Thus I still stand by the fact they’re both just as good and it comes down to personal preference and familiarity.

I welcome reviving the conversation if you have new datapoints of example cases that are problematic to handle in Clojure, but not in Elixir.

Regards

2 Likes

I would guess that the foremost reason clojure is fine with exceptions is that it’s hosted on runtimes that all use exceptions fairly liberally. My feeling is that exceptions tend to be slightly over-used but there’s nothing fundamentally bad about them - it’s useful to have a “bail out” mechanism that works its way up the callstack by default.

The one thing I haven’t seen mentioned in this thread is the Common Lisp extension/alternative of the condition/restart system. It’s a mechanism to allow code up the call stack to decide on what to do in an “exceptional” condition, which gets to pick from a number of strategies provided by the callee. When a condition like that works its way up to the REPL the user/programmer can even pick a strategy at that point and the program can just continue. Pretty magical when you run into it first.

There are a few implementations of the mechanism in the Clojure ecosystem but none have caught on. I’m not really sure why not.

2 Likes

There is an abstraction that is less powerful than a Monad but which already makes your life better: Functors.

Imagine you want to write a function that adds 3 to its argument. Easy.
Now however assume that the argument could be nil in which case you just want to reply with nil.

(defun foo1 (n)
  (when-not (nil? n)
    (+ n 3)))

And now let’s say that you want to write a similar function where you have a list of numbers and want to add 3 to each of them:

(defun foo2 (nums)
  (map #(+ % 3) nums))

Or what if you want to write a function to which you pass another function which either returns a number or which throws an exception?

(defun foo3 (f)
  (try
    (+ (f) 3)
    (catch e ...)))

Or you have a hashmap in which all the values are numbers and you want to add 3 to each of them and thus generate a new map:

(defun foo4 (hm)
  (zipmap (keys hm) (map #(+ % 3) (vals hm))))

In Haskell you would be write an implementation against Functors. In Clojure they exist too, as well as monads. So in Haskell you would express it like this:
foo = fmap (+3)

And in Clojure you could maybe say:
(def foo (partial fmap #(+ % 3)))

And that’s it.
This foo can potentially replace all of the foo versions above. So it is much more reusable. Composability is the holy grail of functional programming and monads compose very well.

We should call out Scott Wlaschin’s amazing series of blog posts and presentations on this topic (amongst many!) which he terms Railway Oriented Programming (ROP). His focus is F# and typed functional languages, and so a little purist compared to what’s required in Clojure. But the principles of the benefits of composition etc are really nicely illustrated. https://fsharpforfunandprofit.com/posts/recipe-part2/

In our team we’ve used Adam Bard’s err->> macro verbatim for a long time. More recently a team member - inspired by Scott’s articles - wrote a Clojure version of ROP. This started off following Scott’s patterns slavishly but then evolved to be more idomatic Clojure. We use both in our code now. https://github.com/HughPowell/railway-oriented-programming

Both show how Clojure can be adaptable to these patterns in a few lines of code without explicitly needing much of the functional apparatus like “map” and “lift” that Scott Wlaschin so nicely defines in his (algebraically) typed FP landscape.

Completely agree with other posters that adoption of this pattern is a choice - there should be no dogma about “good” or “bad” Clojure or FP. However I found this area to be one of those ways into the FP world that I enjoyed and which has brought benefits to our team - i.e. the ease of composition and reduction of magic code paths that comes about with exception habdling.

Note one titbit of advice might be to not catch every exception. If something is vaguely recoverable catch and pass the error down the (monadic) chain - but throw exceptions for truly unexpected and unrecoverable cases. Again probably too pragmatic for some!

2 Likes

This topic was automatically closed 182 days after the last reply. New replies are no longer allowed.