DEV Community

Cover image for Practical Functional Programming in JavaScript - Error Handling
Richard Tong
Richard Tong

Posted on • Edited on

Practical Functional Programming in JavaScript - Error Handling

Hello. You've arrived at an entire post about error handling.

https://imgs.xkcd.com/comics/error_types.png

Comic Credits: https://xkcd.com/2303/

Today we'll talk about Errors in JavaScript functional programming. Errors are about setting expectations, and bugs happen when expectations miss reality. Proper error handling (both throwing and catching) is key to writing code with fewer bugs. In this article, we will explore current and historical methods for JavaScript error handling and attempt to settle on a good general way with current JavaScript syntax to handle Errors. I will also plug a function from my library at the end (with good reason, of course).

Without further ado, let's see what is currently happening with errors in JavaScript functional programming

Feel free to click into these yourself, but I'll save you the trouble - all three articles say something along the lines of "stop throwing errors, instead use the Either monad".

Usually I don't think replacing an part of the language is a good way to go about things, unless the replacement offers something substantially better. Let's make our own judgment by exploring monads. What is a monad?

a monad is an abstraction that allows structuring programs generically while automating away boilerplate code needed by the program logic.

Moreover, monads have a spec. A monad is defined by

  • a type constructor - something with a prototype

function MyMonad(x) {...}

  • a type converter - a way to get a value into a monad

MyMonad.of = x => new MyMonad(x)

  • a combinator - a way to combine multiple instances of a monad

myMonad.chain(anotherMyMonad) -> combinedMyMonad

Now for Either. Here's a minimal Either monad implementation:

function Left(x) {
  this.value = x
}

function Right(x) {
  this.value = x
}

function Either(leftHandler, rightHandler, x) {
  return x.constructor === Left ? leftHandler(x.value) : rightHandler(x.value)
}
Enter fullscreen mode Exit fullscreen mode

Here's how you would use the Either monad.

// parseJSON(s string) -> Either<Left<Error>, Right<Object>>
const parseJSON = s => {
  try {
    return new Right(JSON.parse(s))
  } catch (err) {
    return new Left(err)
  }
}

Either(
  err => console.error(err), // Left
  parsed => console.log(parsed), // Right
  parseJSON('{"a":1,"b":2,"c":3}'),
) // { a: 1, b: 2, c: 3 }
Enter fullscreen mode Exit fullscreen mode

The way with the Either monad certainly looks pure, but is it really any better than a try catch block?

try {
  const parsed = JSON.parse('{"a":1,"b":2,"c":3}')
  console.log(parsed)
} catch (err) {
  console.error(err)
}
Enter fullscreen mode Exit fullscreen mode

Directly above is a vanilla JavaScript try catch block that does everything the Either monad does in the previous example. The snippet above does not require a parseJSON function for Left and Right monads, and is generally more concise. I do not see the benefit of the Either monad when there is already try catch blocks and throw. My opinion is the Either monad doesn't pull enough weight versus regular JavaScript syntax for any serious use. However, I do like that the Either monad promotes a functional style.

There is a similar shortcircuiting pattern to the Either monad in asynchronous callback handlers.

function asyncFunc(userID, cb) {
  getUserByID(userID, (err, user) => {
    if (err) {
      cb(err) // new Left(err)
    } else {
      cb(null, user) // new Right(user)
    }
  })
}

asyncFunc('1', (err, user) => {
  if (err) console.error(err) // Left
  else console.log(user) // Right
})
Enter fullscreen mode Exit fullscreen mode

Left and Right are baked into the syntax of callbacks. If err, do the Left thing, else do the Right thing. This worked well for callbacks, but when Promises came out, a lot of people moved on.

const promiseFunc = userID => new Promise((resolve, reject) => {
  getUserByID(userID, (err, user) => {
    if (err) {
      reject(err) // new Left(err)
    } else {
      resolve(user) // new Right(user)
    }
  })
})

promiseFunc('1')
  .then(user => console.log(user)) // Right
  .catch(err => console.error(err)) // Left
Enter fullscreen mode Exit fullscreen mode

Promises are eerily similar to the Either monad. It's as if Promises were Left, Right, and Either rolled up into one. The thing about Promises though is that they were not created for the sole purpose of expressing a Left and a Right path. Instead, they were created to model asynchronous operations, with left and right paths necessitated by design.

With async/await, we have the latest in error handling

try {
  const user = await promiseFunc('1')
  console.log(user) // Right
} catch (err) {
  console.error(err) // Left
}
Enter fullscreen mode Exit fullscreen mode

With the latest async/await syntax, the try catch block is the current prescribed way to handle errors. If you're happy with try catch blocks, you could stop reading here and be off on your merry way. However, before you go, I should mention there's a clean way to handle errors via a library function (authored by yours truly). Hailing from my functional programming library, rubico, it's tryCatch!

/*
 * @synopsis
 * <T any>tryCatch(
 *   tryer (x T)=>any,
 *   catcher (err Error, x T)=>any,
 * )(x T) -> Promise|any
 */

tryCatch(
  async userID => {
    const user = await promiseFunc(userID)
    console.log(user) // Right
  },
  err => console.error(err), // Left
)('1')

tryCatch(
  jsonString => {
    const parsed = JSON.parse(jsonString)
    console.log(parsed) // { a: 1, b: 2, c: 3 }
  },
  err => console.error(err),
)('{"a":1,"b":2,"c":3}')
Enter fullscreen mode Exit fullscreen mode

rubico's tryCatch is cool because it catches all errors, synchronous or asynchronous. I personally like it because I like only needing one interface to handle all kinds of errors. One could argue that try catch with await would do the same thing, but at that point you're already in Promise land and cannot go back to synchronous land. rubico's tryCatch will behave completely synchronously for a synchronosly thrown Error. The sync vs async Promise correctness of rubico might seem insignificant at first, but it really is nice in practice for this to be a guarantee and not have to worry about it. If you would like to start functional programming on a similar level of bliss, check out rubico today.

Finally, I love monads. I think they're super cool, but they should only be used in places where they actually do something better than you could with vanilla JavaScript. Using monads for the sake of using monads is, well, meh. My belief is JavaScript has its own class of monads - monads that benefit the multi-paradigm language that is JavaScript. If you know of such a monad, I would love to hear about it in the comments.

Thanks for reading! This concludes my series Practical Functional Programming in JavaScript. You can find the rest of the series on rubico's awesome resources. If you have something you would like me to blog about, I would also love to hear it in the comments.

Cover photo credits:
https://resilientblog.co/inspirational/quotes-about-mountains/

Sources:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error
https://en.wikipedia.org/wiki/Monad_(functional_programming)
https://en.wikipedia.org/wiki/Kleisli_category

Top comments (2)

Collapse
 
detzam profile image
webstuff

Question about the try catch.
Should you use it only when you develop the code and after all testing is dobe should you remove it? Or you let it in production?

Collapse
 
richytong profile image
Richard Tong

tryCatch (along with any of the rubico functions) is meant for production use. Here is a fresh benchmark run for tryCatch

noop: 1e+5: 3.224ms
tryCatch_caught_vanilla: 1e+5: 776.34ms
tryCatch_caught_rubicoHappyPath: 1e+5: 772.816ms
tryCatch_caught_rubicoFullInit: 1e+5: 789.817ms
identity: 1e+6: 8.925ms
tryCatch_identity_vanilla: 1e+6: 9.44ms
tryCatch_identity_rubicoHappyPath: 1e+6: 12.431ms
tryCatch_identity_rubicoFullInit: 1e+6: 70.184ms

Here's the benchmark file for tryCatch if you'd like to run the benchmarks. github.com/a-synchronous/rubico/bl...