Hello. You've arrived at an entire post about error handling.
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
- Elegant error handling with the JavaScript Either Monad.
- Functional Error Handling
- Functional Programming in TypeScript
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)
}
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 }
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)
}
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
})
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
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
}
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}')
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)
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?
tryCatch (along with any of the rubico functions) is meant for production use. Here is a fresh benchmark run for tryCatch
Here's the benchmark file for tryCatch if you'd like to run the benchmarks. github.com/a-synchronous/rubico/bl...