DEV Community

Cover image for Result: Composition and Error handling
Patrick Roza
Patrick Roza

Posted on • Originally published at patrickroza.com on

Result: Composition and Error handling

We can improve our error handling and composition by leveraging a Result class and several other tools from the functional programming world.

Instead of throwing errors, we wrap our results. Either the Result is an Error value, or a Success value, in the process documenting the possible errors. Callers must first examine and unwrap the Result, handling either the Success or Failure case. Paving the way for more functional programming and composition.

For a more complete introduction to the Result class and Railway Oriented Programming:

For implementations check GitHub #railway-oriented-programming; ROP in many programming languages (Python/Go/Java/C#/F# etc)

In these series I will share my findings during my (exciting) journey.

Imperative sample

const r = doSomeAction() // Result<string, SomeVariableIsInvalid | ServiceUnavailableError>
if (r.isErr()) { // r: Error<SomeVariableIsInvalid | ServiceUnavailableError>
  if (r.error instanceof SomeVariableIsInvalid) {
    ctx.body = r.error.message
    ctx.statusCode = 400
  } else {
    ctx.statusCode = 500
  }
  return
}
// r: Ok<string>
ctx.body = r.value
ctx.statusCode = 200

doSomeAction could be implemented like:

function doSomeAction(): Result<string, SomeVariableIsInvalid | ServiceUnavailableError> {
  if (!someVariableIsValid) {
    return err(new SomeVariableIsInvalid("some variable is not valid")
  }
  if (!isServiceAvailable()) {
    return err(new ServiceUnavailableError("The service is currently unavailable")
  }

  return ok("success response")
}

Functional sample

doSomeAction() // Result<string, SomeVariableIsInvalid | ServiceUnavailableError>
  .map(value => {
    ctx.body = value
    ctx.statusCode = 200
  })
  .mapErr(error => {
    if (error instanceof SomeVariableIsInvalid) {
      ctx.body = error.message
      ctx.statusCode = 400
    } else {
      ctx.statusCode = 500
    }
  })

All "operators" must live on the Result object and thus extension is harder. (This is similar to how for instance RxJS started)

Functional Composition

doSomeAction() // Result<string, SomeVariableIsInvalid | ServiceUnavailableError>
  .pipe(
    map(value => {
      ctx.body = value
      ctx.statusCode = 200
    }),
    mapErr(error => {
      if (error instanceof SomeVariableIsInvalid) {
        ctx.body = error.message
        ctx.statusCode = 400
      } else {
        ctx.statusCode = 500
      }
    })
  )

The operators are now just functions, easy to extend and roll our own ;-) (RxJS v5.5 users may see some similarities here)

Data last

const pipeline = pipe(
  map(value => {
    ctx.body = value
    ctx.statusCode = 200
  }),
  mapErr(error => {
    if (error instanceof SomeVariableIsInvalid) {
      ctx.body = error.message
      ctx.statusCode = 400
    } else {
      ctx.statusCode = 500
    }
  })
)

pipeline(doSomeAction())

So pipeline is now reusable. If only tc39 proposal-pipeline-operator would land soon, so that we get syntactic sugar that will hide some boiler plate and syntactic noise :)

Building on top

Further decomposition into separate functions, so that they become re-usable, or to separate the levels of abstraction so that the pipeline becomes easier to read.

const writeSuccessResponse = value => {
  ctx.body = value
  ctx.statusCode = 200
}

const writeErrorResponse = error => {
  if (error instanceof SomeVariableIsInvalid) {
    ctx.body = error.message
    ctx.statusCode = 400
  } else {
    ctx.statusCode = 500
  }
}

const pipeline = pipe(
  map(writeSuccessResponse),
  mapErr(writeErrorResponse)
)

Further decomposition:

const writeSuccessResponse = value => {
  ctx.body = value
  ctx.statusCode = 200
}

const writeDefaultErrorResponse = error => {
  ctx.statusCode = 500
}

const writeSomeVariableIsInvalidErrorResponse = error => {
  if (error instanceof SomeVariableIsInvalid) {
    ctx.body = error.message
    ctx.statusCode = 400
  }
}

const pipeline = pipe(
  map(writeSuccessResponse),
  mapErr(writeDefaultErrorResponse),
  mapErr(writeSomeVariableIsInvalidErrorResponse),
)

Perhaps another option:

const mapErrIf = (errorHandler: error => void, predicate: error => boolean) => 
  error => {
    if (!predicate(error)) { return }
    errorHandler(error)
  }
}
// usage
mapErrIf(_ => ctx.statusCode = 400, error => error instanceOf SomeVariableIsInvalid)

And there are of course many other options and forms of composition, let that be the reader's excercise ;-)

Framework and Sample code

I'm working on an application framework while exploring these topics, that leverages the pipeline composition extensively, Sample app included!

Source code:

What's Next

Next in the series, I plan to introduce the more advanced concepts like flatMap, toTup, tee and others :)

Further reading

Be sure to also check out gcanti/fp-ts; a heavily functional programming oriented library, especially v2 is looking very promising due to similar pipe composition!

Top comments (0)