Do or do not, there is no try
Tags: monads, javascript, functional, tutorial
Published: 2023-12-18 19:19:20

---
title: Do or do not, there is no try
published: true
description: 
tags: #monads #javascript #fp #tutorial
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2m1lej2pg0n5d1w4ovcu.png
---
The main building blocks of functional programming are pure functions. Side effects are their worst enemies because they make functions impure. For instance, `JSON.parse` meets nearly all the criteria for a pure function: it always produces the same output for the same input, and it does not depend on the global context or attempt to change it. However, it still harbors a side effect. When an invalid JSON is applied, the function not only fails to return a result but also breaks the application.

```tsx
const bob = JSON.parse('{"name": "Bob"}') // -> Bob
const jackson = JSON.parse('Beat me, hate me') // -> 💥Error!
```

This behavior is by design, and to handle this issue, we need to wrap the function call in a `try/catch` block.

```tsx
let jackson 
try {
  jackson = JSON.parse('You can never break me')
}
catch(error) {
  jackson = 'n/a'
}
```

We had to slightly increase the code and use a mutable variable. Using `let` is not that desirable, as this would require a more careful analysis of the code to track all the places where the variable can be changed, especially when dealing with larger codebases.

I would like to find a more elegant solution to this problem. As an idea, we can try adding an `onError` method to achieve a one-line solution:

```tsx
const jackson = JSON.parse('Beat me, hate me').onError(() => 'n/a')
```

It looks nice, but extending objects or functions through prototypes is playing with fire. Moreover, it's not universally applicable. Every time we need to handle an exception, we first have to modify the original function and then call it. Doesn't sound that great, does it? But what if we create a wrapper function that temporarily extends any function with an `onError` method, without modifying it? In other words, the wrapper function will loan its methods to the wrapped functions, and once the execution is complete, the methods will be returned back to the owner. But don't worry, this will be an interest-free loan, so you won’t have to pay a dime!

```tsx
const jackson = trap(JSON.parse('Beat me...')).onError(() => 'n/a')
```

To make usage even more convenient, we can add pipeline support for function composition, as follows:

```tsx
const userName = trap('You can never break me')
  .then(JSON.parse)
  .then(user => user.name)
  .onError(() => 'n/a')
  .finally()
```

It closely mirrors `Promise`, with the only difference being the `onError` and `catch` methods. Essentially, we can use `Promise` directly without any additional wrapping functions.

```tsx
const parseName = str => Promise.resolve(str)
  .then(JSON.parse)
  .then(user => user.name)
  .then(str => str.trim())
  .then(str => str.toUpperCase())
  .catch(() => 'n/a')
  .finally()

const bob = await parseName('{"name": " Bob  "}') // -> 'BOB'
const sam = await parseName('{"nick": " Sam  "}') // -> 'n/a'
const one = await parseName('{"name": 1}') // -> 'n/a'
const jackson = await parseName('You can never break me') // 'n/a'
```

The sequence of `then` and `catch` functions provides clear control flow and error handling. However, there is a small problem: the `parseName` function is now asynchronous, so the value assignment won't happen immediately after the function execution, but rather on the next event loop tick. This is not what we want. Luckily, we can create a function with the same interfaces as `Promise`, but it will work synchronously.

```tsx
const flow = x => ({
  then: f => trapError(() => f(x)),
  catch: () => flow(x),
  finally: (f = x => x) => f(x),
})

const fail = x => ({
  then: f => fail(x),
  catch: f => trapError(() => f(x)),
  finally: () => {throw x},
})

function trapError(f) {
  try {
    return flow(f())
  }
  catch (error) {
    return fail(error)
  }
}
```

Below is an example of usage. In order to break chaining and get the value, we should call the `finally` method.

```tsx
const findUser = id => flow(localStorage.getItem('users'))
  .then(JSON.parse)
  .then(users => users.find(user => user.id === id))
  .then(user => user.name)
  .catch(() => 'n/a')
  .then(str => str.toUpperCase())
  .finally() // 👈 stop chaining and get value

const bob = findUser(12)
```

Interesting fact: if you call the `flow` function with `await`, it may execute without invoking `finally`, even if the function is synchronous.

```tsx
const ten = flow(7).then(x => x + 3).finally() // -> 10
const two = await flow(1).then(x => x + 1) // -> 2
```

Our function looks and behaves like an asynchronous function, but it does not actually support asynchronous code. This can be misleading. To avoid confusion, I suggest adding support for asynchronicity in the following way:

```tsx
const isPromise = x => x instanceof Promise

const future = p => ({
  then: f => future(p.then(f)),
  catch: f => future(p.catch(f)),
  finally: (f = x => x) => p.then(f),
})

const flow = x => isPromise(x) ? future(x) : ({
  then: f => trapError(() => f(x)),
  catch: () => flow(x),
  finally: (f = x => x) => f(x),
})
```

Now, we can seamlessly mix asynchronous functions in our pipeline without concerns about distinguishing between synchronous and asynchronous code.

```tsx
const refreshToken = salt => flow(localStorage.getItem('app'))
  .then(JSON.parse)
  .then(data => data.session.refreshToken)
  .then(saltedToken => atob(saltedToken).replace(salt, ''))
  .then(token => fetch(`/api/refresh-token/${token}`)) // 👈 now it is async fn
  .then(res => res.json())
  .then(json => json.token)
  .catch(() => window.location.href = '/login')

const token = await refreshToken('Never store token in the local storage!')
```

## Functors and Monads

Without realizing it, we have gradually started using functors in our examples. It might come as a surprise, but the functions `future`, `flow`, and `fail` are actually functors. A functor acts like a container that holds a value, and we can access and potentially modify this value using the function provided to the `then/catch` methods. The functor `flow` recursively calls itself with a new value, allowing us to create endless chains of `then/catch` and build function compositions. Each `then/catch` is enclosed within a `try/catch`, ensuring safe function execution and managing two pathways: a successful path (then) and an error-handling path (catch). To retrieve the final value, we need to invoke the `finally` function, which ends the `then/catch` chain and delivers the result. An interesting feature of the `finally` function is that it can take another function for one last transformation before concluding.

```tsx
const greet = flow('bob')
  .then(str => str.toUpperCase())
  .then(str => 'Hello '.concat(str))
  .finally(str => str.concat('!'))

console.log(greet) // -> Hello BOB!
```

Seems like everything is great, but there is still one thing. If we pass another flow into our pipeline, we will get a functor instead of a result value.

```tsx
// welcome without finally
const welcome = name => flow(name)
  .then(str => str.toUpperCase())
  .then(str => 'Hello '.concat(str))
  .catch(() => 'Oops')

flow('{"name": "Bob"}')
  .then(JSON.parse)
  .then(user => user.name)
  .then(welcome) // 👈 nesting another flow will break 👇
  .finally(console.log) // -> {then: ƒ, catch: ƒ, finally: ƒ}
```

To unfold nested functors, the following changes need to be made:

```tsx
const isFunctor = x => x?.finally instanceof Function

const flow = x => {
  if (isPromise(x)) return future(x)
  if (isFunctor(x)) return x
  return {
    then: f => trapError(() => f(x)),
    catch: () => flow(x),
    finally: (f = x => x) => f(x),
  }
}

const fail = x => ({
  then: (f, r) => r?.(x) ?? fail(x), // 👈 handle fail in promise
  catch: f => trapError(() => f(x)),
  finally: () => {throw x},
})
```

These changes make `flow` a monad. Although it may not be the ideal monad, it possesses some key properties of a monad. This allows us to nest other flows or functors within it.

```tsx
flow('{"name": "Bob"}')
  .then(JSON.parse)
  .then(user => user.name)
  .catch(() => 'guest')
  .then(welcome) // 👈 nesting another flow
  .then(str => str.concat('!'))
  .finally(console.log) // -> Hello BOB!
```

In practice, you most likely don't need to use monads in their pure form. Instead, you will often use them inside functions that return values from the monad. However, there are cases when nesting functors/monads inside `then/catch` methods is reasonable. With the `fail` functor, we can interrupt the execution flow without throwing an exception. It is worth noting that throwing an exception can be a more costly operation compared to a regular function call.

```tsx
const findUser = id => flow(localStorage.getItem('users'))
  .then(JSON.parse)
  .then(users => users.find(user => user.id === id))
  .then(user => user || fail('User not found')) // 👈 jump to the next catch on null
  .then(user => user.name)
  .catch(() => 'n/a')
  .then(str => str.toUpperCase())
  .finally()

const bob = findUser(12)
```

Another example is the `halt` functor, which can be used to skip all chains with a given value. This provides an easy way to end the entire pipeline.

```tsx
const halt = x => ({
  then: () => halt(x),
  catch: () => halt(x),
  finally: (f = x => x) => f(x),
})

const findUser = id => flow(localStorage.getItem('users'))
  .then(JSON.parse)
  .then(users => users.find(user => user.id === id))
  .then(user => user || halt('N/A')) // 👈 quit with value
  .then(user => user.name)
  .then(str => str.toUpperCase())
  .finally()
```

## Conclusion

Is this solution a complete replacement for `try/catch`? I would say that, no, it is not a replacement, but rather a powerful addition for managing code execution and error handling. This approach is convenient in cases where a function can continue its execution despite an error occurring. Additionally, this design pattern can be applied not only for error catching but also for other purposes. Here are some possible applications:

```tsx
const Component = select(role)
  .when('admin', AdminView)
  .when('editor', EditorView)
  .when('user', UserView)
  .otherwise(GuestView)

const selectGrade = mark => select(mark)
  .when(val => val > 90, 'A'),
  .when(between(80, 89), 'B'),
  .when(between(70, 79), 'C'),
  .when(between(50, 69), 'D'),
  .otherwise('F')

const grade = selectGrade(78) // -> C
```

In this article, I intentionally did not follow the conventions and laws of monads. My objective was to introduce readers to an alternative approach to development while highlighting the beauty of monads, which is often hidden beneath academic terminology and excessive boilerplate code. Naturally, it is not possible to cover all possible aspects of this approach in a single article, but I hope that the information shared here will serve as a starting point for you to explore the world of monads.



P.S. You can find the [online demo here](https://codesandbox.io/p/sandbox/do-or-donot-7gml4s) 📺.