Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to handle errors in Ramda

I am pretty new to Ramda and functional programming and trying to rewrite a script with Ramda but am unsure of how to handle errors with Ramda in a clean way. This is what I have, does anyone have any pointers with how to rewrite this in a functional way using Ramda?

const targetColumnIndexes = targetColumns.map(h => {
    if (header.indexOf(h) == -1) {
      throw new Error(`Target Column Name not found in CSV header column: ${h}`)
    }
    return header.indexOf(h)
  })

For reference, these are the values of header and targetColumns

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

So I need to:

  • map over targetColumns
  • return the indexOf the targetColumn from header
  • throw an error if the index is -1
like image 852
Le Moi Avatar asked Mar 19 '19 11:03

Le Moi


People also ask

What is Ramda used for?

Ramda is a library of functions designed to make functional programming in JavaScript easy and powerful without making it any less like JavaScript.

What is compose in ramda?

compose FunctionPerforms right-to-left function composition. The rightmost function may have any arity; the remaining functions must be unary.

Should I use Lambda or step functions for error handling?

However, it might add quite a significant overhead. AWS Lambda error handling can be done in different ways, like utilizing wrappers. On the other hand, AWS Step Functions have proved to be incredibly beneficial for building a serverless application that’ll deal with retries and errors appropriately, making Step Functions an effective solution.

What is Lambda error in AWS Lambda?

A Lambda function will generate errors randomly when found within the application. If CloudWatch Logs detect the word ERROR within the function’s logs, it’ll provide the processor function with an event for processing. The data has details about the log event when it’s decoded.

What happens when Lambda fails?

When failure occurs, and it will occur at some point, you’ll most likely notice Lambda retries based on these behaviors: Stream-based events – if the current events are solely DynamoDB streams and AWS Kinesis Data Streams.

What causes unhandled exceptions in lambda?

Raised unhandled exception — can happen because of a programming bug, failure of an external API, or if you’ve received an invalid input. Timeout — ‘Task timed out after X seconds’ message appears when Lambda closes violently because it ran longer than the pre-configured timeout duration.


1 Answers

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.

like image 187
Scott Sauyet Avatar answered Oct 25 '22 13:10

Scott Sauyet