DEV Community

loading...

Why Functional Programmers Avoid Exceptions

Jesse Warden
I write code, front end and back-end, and like deploying it on AWS. Software Developer for 20 years, and still love it. Amateur Powerlifter & Parkourist.
Originally published at jessewarden.com ・9 min read

If you’re in a hurry, here is the 60 second version:

My previous article caused a variety of consternation, imperative patriotism, and lots of nuanced follow up. It reminded me of when Richard Feynman was asked to define how magnets work and he refused. The perturbed interviewer postulated it was a reasonable question in hopes to understand why Mr. Feynman wouldn’t answer it. Richard Feynman covered a variety of reasons, 2 of which were:

  1. you have to know the deeper reasons first before I can explain it
  2. I can’t cheat by using analogies that they themselves require deeper meanings to explain how _they_ work.

In the case of avoiding async/await keywords in JavaScript, this makes a huge assumption you know about Functional Programming, Imperative, exception handling, how various languages approach it or don’t, the challenges between dynamic and strongly typed languages, and on and on.

In this article, I wanted to remedy that and focus on the deeper reasons why, specifically being pragmatic around how Functional Programmers get things done vs. the theory or why’s. This means understanding:

  • why pure functions are preferred
  • how they’re easier to test
  • why you return errors as values using Result/Either types
  • how you compose software using them

Pedantic or Mathematical Answer

In investigating specifically why exceptions aren’t preferred in Functional Programming, I found out, they aren’t actually anti-functional programming. Worse, I found out many argue they do not violate pure functions or referential transparency with a lot of fascinating supporting evidence. A few argue they aren’t even side effects. It gets more confusing when you start comparing strictly typed functional languages vs. dynamic ones, or practicing FP in non-FP languages.

In practice, exceptions, like side effects, seem to violate all the reasons why you use pure functions: Your code is predictable, easier to test, and results in better software. Exceptions ensure your code is unpredictable, reduces the value of the tests, and results in worse software. Yet that’s not what the mathematical definitions say. They don’t agree, nor disagree with my assertions; rather they just say that known exceptions do not violate referential transparency. Yes, there are detractors. Regardless, this really shook my faith.

One could say these are pedantic; citing the true definition of referential transparency the mechanisms behind how Exceptions can or cannot negatively affect it, and thus possibly not violate pure function rules. However, this is the common problem between scientists and engineers: while scientists will give you the Mathematicians Answer, they won’t help you do your actual job.

And that’s what brought me back to reality. I’m not here to debate semantics, I’m here to deliver working software. However, I will cede to nuance if someone wishes to delve into the relationships between the mathematics behind these constructs. So far, preferring mathematical style programming over Imperative or Object Oriented seems to be going much better in delivering better results even if I don’t have a 100% iron clad understanding of all the nuances of the rules.

The good news, despite finding deep nuance around exceptions and their complicated relationship with the mathematical purity of FP the industry, both FP and others (i.e. Go, Rust, Lua) has basically accepted the pragmatic truth: exceptions aren’t pure, act like side effects, and aren’t helpful when writing software. We already have a solution: returning the errors as values from functions, using Result (or Either) types.

Keep in mind, the above has a Haskell bias. I encourage you to google “Exceptions Considered Harmful” and see some of the horrors that can arise when exceptions put your stateful code (Java/C#/Python/JavaScript) into a bad state.

Prefer Pure Functions

When people say prefer pure functions it’s because of the following reasons:

  • more predictable
  • easier to test
  • easier to maintain

What does that mean, though?

Predictable

We say predictable because you call it and it returns a value. That’s it.

const isAnOk = safeParseJSON('{"foo": "bar"}')
const isAnError = safeParseJSON('')
Enter fullscreen mode Exit fullscreen mode

When you bring exceptions into it, you now have 2 possibilities: it either returns a value, or blows up.

const result = JSON.parse('') // result is never used/set
Enter fullscreen mode Exit fullscreen mode

When you combine functions together into programs, the program takes a value and returns a value. That’s it.

When you bring exceptions into it, you now have X * Y possibilities: the program either returns a value, or X number of functions possibly explode in Y number of ways; it depends on how you wire the functions together.

This exponential complexity shows just how unpredictable code can be with exceptions.

Easier To Test

Easier compared to what? How?

Pure functions don’t have side effects, so you don’t have to setup and tear down stubs or mocks. There is no initial state to setup, nor state to reset afterwards. There is no spy that you have to assert on after you call your code.

Instead, you give your function an input, and assert the output is what you expect.

expect(safeParseJSON('{"foo": "bar"}')).to.be(Ok)
expect(safeParseJSON('')).to.be(Error)
Enter fullscreen mode Exit fullscreen mode

Easier to Maintain

Compared to what? What does “easier” mean? Easy for someone familiar with the code? This statement is too nebulous and full of feelings.

Still, many would agree, regardless of language, that code that doesn’t have any side effects is a lot easier to deal with and change and unit test over 6 months of the code growing compared to one that has a lot of side effects that you have to account for, test, and learn about their possible exponential changes in the code.

Use Result/Either

If you prefer pure functions, that means very little side effects, or they’re on the fringes of your code. But then how do you handle things that go wrong? You return if the function worked or not. If it worked, it’ll have the data inside. If it failed, it’ll have a reason why it failed. In FP languages they have a Result or Either type. In languages that don’t have this kind of type, you can emulate in a variety of ways. If the code works, you return an Ok with the value in it. If the function failed, you return an Error with the reason why as a string clearly written in it.

const safeParseJSON = string => {
    try {
        const result = JSON.parse(string)
        return Result.Ok(result)
    } catch(error) {
        return Result.Error(error?.message)
    }
}
Enter fullscreen mode Exit fullscreen mode

Many languages have embraced the Promise, also called a Future, way of doing things. Some languages have used this to also handle asynchronous operations because they can fail in 2 ways that mean the same thing: it broke or it timed out. For example, most people aren’t going to wait 10 minutes for their email to come up, so you typically will see failures within 10 to 30 seconds even though technically nothing went wrong; we just stopped trying after a set amount of time. JavaScript and Python’s versions don’t have this timing built in, but there are libraries that allow to use this behavior.

This results in pure functions that always return a value: a Result. That can either be a success or failure, but it’s always a Result. If it’s a failure it won’t break your entire program, nor cause you to have to write try/catch. While Promises can substitute in for a Result in JavaScript for example, ensure you are using the Promise itself, and not the value it returns via async/await. That completely bypasses the built-in exception handling, and forces you to use try/catch again.

Composing Programs

The way you build FP programs is through combining all these pure functions together. Some can be done imperatively, sure, but most are done via some type of railway oriented programming. There are variety of ways to do this in FP and non-FP languages:

This means, in ReScript and F#, you’ll have a function, and a Result will come out. You can then see if your program worked or not.

let parsePeople = str =>
    parsePeopleString(str) // <-- this function could be an Ok or Error
    -> filterHumans
    -> formatNames
    -> startCaseNames
Enter fullscreen mode Exit fullscreen mode

For JavaScript/Python, it’s a bit more nuanced around the types. For Python, we’ll assume you’re returning a Result in PyMonad or Returns.

def parse_people(str):
  return parse_people_string(str)
  .then(filter_humans)
  .then(format_names)
  .then(start_case_names)
Enter fullscreen mode Exit fullscreen mode

Composing JavaScript via Promises

For JavaScript, unless you’re all-in on some kind of library, natively you can do this using Promise. Promise is already a type of Result: it holds a value, and if it worked, you can get it out using then, else the failure via catch. They’re also composable by default so you can create Promise chains that automatically unwrap Promise values, use regular values as is, or abort to the catch in case of an error. You lose that ability once you start using async await because now you’re responsible for:

  • exception handling
  • pulling the value out
  • if it’s a Promise, async/awaiting it
  • if it’s a value, using it
  • putting into the next function down the line
  • handling what to do if you get an exception at each section of the code

For Promises, you just return a value or another Promise and it just comes out the other end ready to go. If not, you’re catch will handle any errors. This ensures whatever function calls your Promise chain itself is pure because it always returns a Promise value.

2 huge assumptions:

  1. you’re always defining a catch
  2. you’re not using a Result

Mixing in Result

If some functions aren’t asynchronous, most JavaScript programmers would think they can just return a Result type instead to keep it synchronous. There isn’t a huge penalty in speed/memory to using a Promise, but some would prefer to use a Result instead. I’d suggest to 2 things if you’re not using a library: favor a Promise over a Result. A Promise is native and basically acts like a result already.

const parseJSONSafe = string => {
  try {
    const result = JSON.parse(result)
    return Promise.resolve(result)
  } catch(error) {
    return Promise.reject(error)
  }
}
Enter fullscreen mode Exit fullscreen mode

If, however, you’d prefer to make a clear delineation between an async operation and a possible failure scenario, then you’ll have to unwrap it at the end of the promise chain, similar to Rust or Python’s dry/returns. There are many helper methods on how to do this based on what Result library you’re using. We’ll use Folktale below. Here we’ve defined a safe wrapper around JSON.parse:

const parseJSONSafe = string => {
  try {
    const result = JSON.parse(result)
    return Ok(result)
  } catch(error) {
    return Failure(error)
  }
}
Enter fullscreen mode Exit fullscreen mode

When using it, it’ll come out the next Promise’ then and we can pattern match to get the error or value out and convert to a normal Promise.

const parse = () =>
  fetchJSON()
  .then(parseJSONSafe)
  .then(
    result =>
      result.matchWith({
        Failure: ({ value }) => Promise.reject(new Error(value)),
        Ok: ({ value }) => Promise.resolve(value)
  )
Enter fullscreen mode Exit fullscreen mode

Conclusions

Functional Programmers avoid exceptions because they basically act like side effects, tend to feel like they’re violating pure function rules in regards to having no return value and possibly crashing our program. If you instead favor pure functions, return a Result type when things can possibly fail. You can then use your language’s preferred way of composing functions together. Then you have pure programs that have an input and an output. This means both the functions, and the program itself, are much easier to unit test. You no longer have to write expect(thisThing).throws(SomeExceptionType). You don’t have to write try/catch/throw in your code. You just give your functions or program and input, and assert on that output.

For side effects, if you can’t force them to return a meaningful value, then you can just assert they were called with your expected inputs via Sinon’s spy methods or TestDouble’s assert method. There is no longer indirection, no longer a need to use to try/catch in multiple places for your code. This makes your functions and program much more predictable, especially when you combine many functions together.

For native functionality in non-functional languages like JavaScript and Python, you wrap the unsafe code. In the above examples, we wrapped JSON.parse with a try/catch and have it either return a Result or Promise. In FP languages, that would already return a Result. If you’re programmatic, languages like ReScript and F# support both Result types AND pattern matching on exceptions (which I think is blasphemy).

Discussion (10)

Collapse
hanpari profile image
Pavel Morava

I saw even articles dealing with OOP discouraging exceptions and offering patterns how to avoid it.

Exceptions are disguised gotos and as such they promote unsafe design.

I mean there is no reason to differentiate between paradigms as exceptions are never good idea.

I am old enough to remember when exceptions were hailed as a modern safer approach, replacing the old procedural error codes in C or PHP or whatever.

The problem with OOP languages that are plagued with statements is the impossibility to deal with errors in other way than by firing a side effect.

Especially constructors with the new keyword, like in Java, prevent any reasonable handling of errors if you do not want to make constructors private, and even then, those languages lacked typing system advanced enough to handle the error states properly.

So one had to implement adhoc objects as a workaround that was such an hassle that people preferred throwing exceptions.

But once again, there is no reason to discourage side effects as something unique to functional programming.

In my opinion, exceptions should be consider code smell no matter the language involved.

Collapse
jesterxl profile image
Jesse Warden Author • Edited

It's funny you say that:

I am old enough to remember when exceptions were hailed as a modern safer approach

... am I going to say this in 20 years!?!:

I am old enough to remember when Elm didn't have side effects at all, and it was hailed as a safer, easier approach.

I HOPE NOT lol!

Collapse
hanpari profile image
Pavel Morava

You won't, probably.

On the other hand, I guess than in twenty years the horrible era of today's languages will be over. :)

Thread Thread
jesterxl profile image
Jesse Warden Author • Edited

hahahah.... Dude, COBOL still exists in AWS Lambdas, we're screwed.

Collapse
macsikora profile image
Pragmatic Maciej

Everything depends from the context. If you work with highly procedural code which for example does db queries (yes just calls db directly, do not use any monads and so on), then you don't work with pure functions at all, or you have them very limited, and your program is just composition of smaller programs/procedures. Therefore in such a code using Either to avoid "exploding" has really not a big sense, as to do so you need to in every such procedure just catch everything (what you did in your parseJSON example) which can explode and transfer it into Either. That ends by polluted code which naturally is side effectful but we just remove one side effect of it - error handling. In effect we have still procedural code, but code which in some level has errors as values.

Also I highly not like your comparison to Feynman, where you assume anybody who criticised your text just cannot understand what you are saying, and you able to show everything in 60 seconds video 🤷‍♂️

As said previous, that is a good idea to have value as error, but without seeing wider context we can think we live in the bubble without side effects, what very often is not true in mainstream languages. As mentioned in example, if I query the server in the function, what is already a side effect, having no exception do not make me pure function.

I also very often see Mathematics as a way to explain why FP is so better. As of course Mathematics is a background of everything we do, but really it is in background of everything humans are doing, you want it or not. FP is just simpler represent in Mathematical notation, where procedural programming is closer to original turing machine, and sequence of steps, what is also based on Mathematics 😉. So the fact that FP can be easier describe in math doesn't specially make it better.

And to be clear:

  • errors as values
  • referential transparency
  • function composition

Are good things. But it depends from the context and project style of programming how deep we go into that. Also:

const value = parseMe(data)
const result = checkResult(value)
Enter fullscreen mode Exit fullscreen mode

is the same composition like

const result = pipe(data, parseMe, checkResult)
Enter fullscreen mode Exit fullscreen mode

This is exactly the same, we just have local aliases for values in first example, and we don't have them in second. Everything is about - effects and mutation, and not about syntax.

And in the end in purely FP code I would not use exceptions, as exactly how you said they are against FP. There is no argument about that. But the worst I can imagine is mixing Eithers and exceptions in one codebase, this really can end badly. And in JS/TS/Python kind of projects there is almost no way to avoid exceptions.

Collapse
jesterxl profile image
Jesse Warden Author • Edited

You wrap the side effects because they throw. If you didn't, they'd throw. You instead return a Result, which is composable. You can also return a Promise, which is also composeable. These aren't procedural, they're functions you can combine together like one -> two -> three or Promise.resolve(value).then(something).then(another). If you don't return a Result or a Promise, then yes, it's procedural.

Please assume benevolent intent. My comparison to Feynman was realizing I should of defined why Functional Programming eskews throwing Exceptions, and instead returns results from functions. If you don't know that context beforehand, then the ideas seem strange in async/await syntax.

If you call a server, you can return a value and have it not throw. You simple wrap the catch, and say "it didn't work". Why it didn't work is important, but beyond status code and response.ok, the fetch interface doesn't give you much. You'll have to do all that work yourself around status code, protected data decoding vs. the common response.json(). If you do that, then you have a better error message that comes out, but the function is still pure: "it works or it doesn't". The side effect, fetch, you can use Dependency Injection or the Effect pattern (or mocks to test). In Elm, there are no side effects so this isn't a problem. In ReScript, it's quite interesting; they follow JavaScript side effects model, but still strive for pure functions with "mostly" mutability. Still, even in ReScript, you'd return something like Elm: some kind of type to pattern match on like "It worked" "bad status code" "bad data" or "network error". However, you have to like the functional style and pattern matching to do that. If you don't want to do all that work in non-FP languages like JavaScript/Python, that's fine.

I'm not saying FP is better. I'm saying this style is easier for me and others. If you like it, cool, here's how you do it. If you don't, cool.

Your examples aren't the same. It's same reason Go developers are ok with if err != nil everywhere, which I get; they like the simple and explicit. You manually have to create the variables value, then manually call the function checkResult, then put the value in it, then at some point wrap some of that with a try/catch. Promises or Result.bind do all that for you without a need of try/catch.

Collapse
macsikora profile image
Pragmatic Maciej • Edited

Promise is composable but also does eagerly side effects and if we define function as something which is referencialy transparent then Promise is not. It return depends from the "state of the world" or just depends from time. And it makes it procedure more than a function, procedure which returns some value to the caller.
Things which return value are composable but without monads doing composition with values wrapped by Either would look like your Go example where every function would check if we have left or right.

And such Either Monad allows for composition positive path totally not thinking that there is any Either and possible error.

When we look on this though we see that it is exactly simulation of procedural exception which allows code to work in positive path and just goto catch for errors. Even exactly that was one of reasons Monads in Haskell were such a thing. They allowed for pure functional composition which could do the same staff like procedural code.

Collapse
gdenn profile image
Dennis Groß

I also reached a point in my career where I am convinced that writing as much pure code (in a functional programming sense) as possible-/feasable is the way to go.

It just delivers code that is easier to test, does not require deep understanding of every function (absence of side effects) and contributes to the overall code quality.

But as you already wrote, throwing exceptions is not contradictory to the functional programming paradigm as long as I declare what can be thrown in the function header.

In this sense, exceptions become just another return value of the function. At the end the way I access these "Exception return values" differs.

Problematic are the languages that cannot declare what Exceptions may be raised e.g. Python, Ruby...etc.

I think using real Ok -/ Error return values for such languages makes 100% sense. I also would not advocate against it in languages such as Java where we can declare what exceptions can be expected from a function. But throwing exceptions in such languages is a fine practice nevertheless (my opinion of course).

Collapse
iquardt profile image
Iven Marquardt • Edited

If an exception would include a mechanism to resume the function at the same position it was originally raised in, then such exceptions were considered pure. This is actually the way algebraic effects work, though I am not an expert on this topic.

Collapse
jesterxl profile image
Jesse Warden Author

Me neither. I know React does something similar to Eff/Koka where they throw Promises. Bizarre stuff using the stack to unwind for control logic.