How to handle errors in Ramda

As customcommander says, there is a good reason that this style of throwing exceptions is not made easy by functional programming: it's much harder to reason about.

"What does you function return?"

"A number."

"Always?"

"Yes, ... well unless it throws an exception."

"Then what does it return?"

"Well it doesn't."

"So it returns a number or nothing at all?"

"I guess so."

"Hmmm."

One of the most common operations in functional programming is composing two functions. But that only works if the output of one function match the input of its successor. This is difficult if the first one might throw an exception.

To deal with this the FP world uses types that capture the notions of failure. You may have seen talk of the Maybe type, which handles values that might be null. Another common one is Either (sometimes Result), which has two subtypes, for the error case and the success one (respectively Left and Right for Either or Error and Ok for Result.) In these types, the first error found is captured and passed down the line to whoever needs it, while the success case continues to process. (There are also Validation types that capture a list of errors.)

There are many implementations of these types. See the fantasy-land list for some suggestions.

Ramda used to have its own set of these types, but has backed away from maintaining it. Folktale and Sanctuary are the ones we often recommend for this. But even Ramda's old implementation should do. This version uses Folktale's data.either as it's one I know better, but later versions of Folktale replace this with a Result.

The following code block shows how I might use Eithers to handle this notion of failure, especially how we can use R.sequence to convert an array of Eithers into an Either holding an array. If the input includes any Lefts, the output is just a Left. If it's all Rights, then the output is a Right containing an array of their values. With this we can convert all our column names into Eithers that capture the value or the error, but then combine them into a single result.

The thing to note is that there are no exceptions thrown here. Our functions will compose properly. The notion of failure is encapsulated in the type.

const header = [ 'CurrencyCode', 'Name', 'CountryCode' ]

const getIndices = (header) => (targetColumns) => 
  map((h, idx = header.indexOf(h)) => idx > -1
    ? Right(idx)
    : Left(`Target Column Name not found in CSV header column: ${h}`)
  )(targetColumns)

const getTargetIndices = getIndices(header)

// ----------

const goodIndices = getTargetIndices(['CurrencyCode', 'Name'])

console.log('============================================')
console.log(map(i => i.toString(), goodIndices))  //~> [Right(0), Right(1)]
console.log(map(i => i.isLeft, goodIndices))      //~> [false, false]
console.log(map(i => i.isRight, goodIndices))     //~> [true, true]
console.log(map(i => i.value, goodIndices))       //~> [0, 1]

console.log('--------------------------------------------')

const allGoods = sequence(of, goodIndices)

console.log(allGoods.toString())                  //~> Right([0, 1])
console.log(allGoods.isLeft)                      //~> false
console.log(allGoods.isRight)                     //~> true
console.log(allGoods.value)                       //~> [0, 1]

console.log('============================================')

//----------

const badIndices = getTargetIndices(['CurrencyCode', 'Name', 'FooBar'])

console.log('============================================')
console.log(map(i => i.toString(), badIndices))   //~> [Right(0), Right(1), Left('Target Column Name not found in CSV header column: FooBar')
console.log(map(i => i.isLeft, badIndices))       //~> [false, false, true]
console.log(map(i => i.isRight, badIndices))      //~> [true, true, false]
console.log(map(i => i.value, badIndices))        //~> [0, 1, 'Target Column Name not found in CSV header column: FooBar']


console.log('--------------------------------------------')

const allBads = sequence(of, badIndices)          
console.log(allBads.toString())                   //~> Left('Target Column Name not found in CSV header column: FooBar')
console.log(allBads.isLeft)                       //~> true
console.log(allBads.isRight)                      //~> false
console.log(allBads.value)                        //~> 'Target Column Name not found in CSV header column: FooBar'
console.log('============================================')
.as-console-wrapper {height: 100% !important}
<script src="//bundle.run/[email protected]"></script>
<!--script src="//bundle.run/[email protected]"></script-->
<script src="//bundle.run/[email protected]"></script>
<script>
const {map, includes, sequence} = ramda
const Either = data_either;
const {Left, Right, of} = Either
</script>

The main point to me is that values such as goodIndices and badIndices are useful on their own. If we want to do more processing with them, we can simply map over them. Note for instance that

map(n => n * n, Right(5))     //=> Right(25)
map(n => n * n, Left('oops')) //=> Left('oops'))

So our errors are left alone and our successes are processed further.

map(map(n => n + 1), badIndices) 
//=> [Right(1), Right(2), Left('Target Column Name not found in CSV header column: FooBar')]

And this is what these types are all about.