In the previous article we already gained some intuition regarding monadic error handling with Promise
, it is time for us to move forward. JavaScript doesn't have native solutions for monadic error handling beyond Promise
, but there are many libraries which helps to fulfil the functionality. amonad has the most similar to the Promise
API. Therefore it is going to be used for the following examples.
Abstraction which represents the result of computations which can possibly fail is commonly known as Result
. It is like immediately resolved Promise
. It can be represented by two values: Success
contains expected information, while Failure
has the reason for the error. Moreover, there is Maybe
as known as Option
which also embodied by two kinds: Just
and None
. The first one works in the same way as Success
. The second one is not even able to carry information about the reason for value's absents. It is just a placeholder indicated missing data.
Creation
Maybe
and Result
values can be instantiated via factory functions. Different ways to create them are presented in the following code snippet.
const just = Just(3.14159265)
const none = None<number>()
const success = Success<string, Error>("Iron Man")
const failure: Failure<string, Error> =
Failure( new Error("Does not exist.") )
NaN
safe division function can be created using this library in the way demonstrated below. In that way, the possibility of error is embedded in the return value.
const divide = (
numerator: number,
quotient: number
): Result<number, string> =>
quotient !== 0 ?
Success( numerator/quotient )
:
Failure("It is not possible to divide by 0")
Data handling
Similarly to Promise
, Result
and Maybe
also have then()
. It also accepts two callback: one for operations over enclosed value and other one dedicated for error handling. The method returns a new container with values processed by provided callbacks. The callbacks can return a modified value of arbitrary type or arbitrary type inside of similar kind of wrapper.
// converts number value to string
const eNumberStr: Maybe<string> = Just(2.7182818284)
.then(
eNumber => `E number is: ${eNumber}`
)
// checks if string is valid and turns the monad to None if not
const validValue = Just<string>(inputStr)
.then( str =>
isValid(inputStr) ?
str
:
None<string>()
)
Besides that due to the inability of dealing with asynchronism, availability of enclosed value is instantly known. Therefore, it can checked by isJust()
and isSuccess()
methods.
Moreover, the API can be extended by a number methods to unwrap a value: get()
, getOrElse()
and getOrThrow()
. get()
output is a union type of the value type and the error one for Result
and the union type of the value and undefined
for Maybe
.
// it is also possible to write it via isJust(maybe)
if( maybe.isJust() ) {
// return the value here
const value = maybe.get();
// Some other actions...
} else {
// it does not make sense to call get()
// here since the output is going to be undefined
// Some other actions...
}
// it is also possible to write it via isSuccess(result)
if( result.isSuccess() ) {
// return the value here
const value = result.get();
// Some other actions...
} else {
// return the error here
const error = result.get();
// Some other actions...
}
Error handling
The second argument of the then()
method is a callback responsible for the handling of unexpected behaviour. It works a bit differently for Result
and Maybe
.
In the case of None
, it has no value, that's why its callback doesn't have an argument. Additionally, it doesn't accept mapping to the deal, since it should produce another None
which also cannot contain any data. Although, it can be recovered by returning some fallback value inside of Maybe
.
In the case of Failure
, the second handler works a bit similar to the first one. It accepts two kinds of output values: the value of Throwable as well as anything wrapped by Result
.
Additionally, both of them are also capable of handling callbacks returning a void
, it can be utilized to perform some side effect, for example, logging.
// tries to divide number e by n,
// recoveries to Infinity if division is not possible
const eDividedByN: Failure<string, string> =
divide(2.7182818284, n)
.then(
eNumber => `E number divided by n is: ${eNumber}`,
error => Success(Infinity)
)
// looks up color from a dictionary by key,
// if color is not available falls back to black
const valueFrom = colorDictionary.get(key)
.then(
undefined,
() => "#000000"
)
Similarly to previous situations, it is also possible to verify if the value is Failure
or None
via isNone()
and isFailure()
methods.
// it is also possible to write it via isNone(maybe)
if(maybe.isNone()) {
// it does not make sense to call get()
// here since the output is going to be undefined
// Some other actions...
} else {
// return the value here
const value = maybe.get();
// Some other actions...
}
// it is also possible to write it via isFailure(result)
if(result.isFailure()) {
// return the error here
const error = result.get();
// Some other actions...
} else {
// return the value here
const value = result.get();
// Some other actions...
}
Which one should be used?
Typical usage of Maybe
and Result
is very similar. Sometimes it is hardly possible to make a choice, but as it was already mentioned there is a clear semantic difference in their meanings.
Maybe
, primary, should represent values which might not be available by design. The most obvious example is the return type of Dictionary
:
interface Dictionary<K, V> {
set(key: K, value: V): void
get(key: K): Maybe<V>
}
It can also be used as a representation of optional value. The following example shows the way to model a User
type with Maybe
. Some nationalities have a second name as an essential part of their identity others not. Therefore the value can nicely be treated as Maybe<string>
.
interface Client {
name: string
secondName: Maybe<string>
lastName: string
}
The approach will enable implementation of client's formatting as a string the following way.
class VIPClient {
// some implementation
toString() {
return "VIP: " +
this.name +
// returns second name surrounded
// by spaces or just a space
this.secondName
.then( secondName => ` ${secondName} ` )
.getOrElse(" ") +
this.lastName
}
}
Computations which might fail due to obvious reason are also a good application for Maybe
. Lowest common denominator might be unavailable. That is why the signature makes perfect sense for getLCD()
function:
getLCD(num1: number, num2: number): Maybe<number>
Result
is mainly used for the representation of value which might be unavailable for multiple reasons or for tagging of a data which absents can significantly affect execution flow.
For example, some piece of class’s state, required for computation, might be configured via an input provided during life-circle of the object. In this case, the default status of the property can be represented by Failure
which would clarify, that computation is not possible until the state is not initialized. Following example demonstrates the described scenario. The method will return the result of the calculation as Success
or “Data is not initialized” error message as Failure
.
class ResultExample {
value: Result<Value, string> = Failure(“Data is not initialized”)
init( value: Value ) {
this.value = Success(value)
}
calculateSomethingBasedOnValue(){
return this.value.then( value =>
someValueBasedComputation( value, otherArgs)
)
}
}
Moreover, Result
can replace exceptions as the primary solution for error propagation. Following example presents a possible type signature for a parsing function which utilizes Result
as a return type.
parseUser( str: string ): Result<Data>
The output of such a function might contain processed value as Success
or an explanation of an error as Failure
.
Conclusion
Promise
, Result
and Maybe
are three examples of monadic containers capable of handling missing data. Maybe
is the most simple one, it is able to represent a missing value. Result
is also capable to tag a missing value with an error message. Promise
naturally extends them with an ability to represent data which might become available later. Moreover, it can never become available at all. That might happen due to error which can be specifically passed in case of rejection. So, Promise
is the superior one and it can basically model all of them. However, specificity helps to be more expressive and efficient.
This approach to error handling is a paradigm shift since it prevents engineers from treating errors as exceptional situations. It helps to express them as an essential part of the execution. You know, from time to time all of us fails. So in my mind, it is wise to follow a known principle: "If you are going to fail, fail fast".
Top comments (0)