An Interest In:
Web News this Week
- April 26, 2024
- April 25, 2024
- April 24, 2024
- April 23, 2024
- April 22, 2024
- April 21, 2024
- April 20, 2024
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:
- https://fsharpforfunandprofit.com/rop/
- https://dev.to/_gdelgado/type-safe-error-handling-in-typescript-1p4n
- https://khalilstemmler.com/articles/enterprise-typescript-nodejs/handling-errors-result-class/
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.valuectx.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) }}// usagemapErrIf(_ => 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:
- fp-app framework
- sample app
- neverthrow pipe extensions
- Uses neverthrow fork (forked from gDelgado14/neverthrow)
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!
Original Link: https://dev.to/patroza/result-composition-and-error-handling-m8i
Dev To
An online community for sharing and discovering great ideas, having debates, and making friendsMore About this Source Visit Dev To