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 Either
s 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 Left
s, the output is just a Left
. If it's all Right
s, then the output is a Right
containing an array of their values. With this we can convert all our column names into Either
s 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.