In the previous article we already gained some intuition regarding monadic error handling withPromise
, it is time for us to move forward. JavaScript doesn't have native solutions for monadic error handling beyondPromise
, but there are many libraries which helps to fulfil the functionality.amonad has the most similar to thePromise
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 asResult
. It is like immediately resolvedPromise
. It can be represented by two values:Success
contains expected information, whileFailure
has the reason for the error. Moreover, there isMaybe
as known asOption
which also embodied by two kinds:Just
andNone
. The first one works in the same way asSuccess
. 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
andResult
values can be instantiated via factory functions. Different ways to create them are presented in the following code snippet.
constjust=Just(3.14159265)constnone=None<number>()constsuccess=Success<string,Error>("Iron Man")constfailure:Failure<string,Error>=Failure(newError("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.
constdivide=(numerator:number,quotient:number):Result<number,string>=>quotient!==0?Success(numerator/quotient):Failure("It is not possible to divide by 0")
Data handling
Similarly toPromise
,Result
andMaybe
also havethen()
. 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 stringconsteNumberStr:Maybe<string>=Just(2.7182818284).then(eNumber=>`E number is:${eNumber}`)// checks if string is valid and turns the monad to None if notconstvalidValue=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 byisJust()
andisSuccess()
methods.
Moreover, the API can be extended by a number methods to unwrap a value:get()
,getOrElse()
andgetOrThrow()
.get()
output is a union type of the value type and the error one forResult
and the union type of the value andundefined
forMaybe
.
// it is also possible to write it via isJust(maybe)if(maybe.isJust()){// return the value hereconstvalue=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 hereconstvalue=result.get();// Some other actions...}else{// return the error hereconsterror=result.get();// Some other actions...}
Error handling
The second argument of thethen()
method is a callback responsible for the handling of unexpected behaviour. It works a bit differently forResult
andMaybe
.
In the case ofNone
, 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 anotherNone
which also cannot contain any data. Although, it can be recovered by returning some fallback value inside ofMaybe
.
In the case ofFailure
, 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 byResult
.
Additionally, both of them are also capable of handling callbacks returning avoid
, 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 possibleconsteDividedByN: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 blackconstvalueFrom=colorDictionary.get(key).then(undefined,()=>"#000000")
Similarly to previous situations, it is also possible to verify if the value isFailure
orNone
viaisNone()
andisFailure()
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 hereconstvalue=maybe.get();// Some other actions...}
// it is also possible to write it via isFailure(result)if(result.isFailure()){// return the error hereconsterror=result.get();// Some other actions...}else{// return the value hereconstvalue=result.get();// Some other actions...}
Which one should be used?
Typical usage ofMaybe
andResult
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 ofDictionary
:
interfaceDictionary<K,V>{set(key:K,value:V):voidget(key:K):Maybe<V>}
It can also be used as a representation of optional value. The following example shows the way to model aUser
type withMaybe
. Some nationalities have a second name as an essential part of their identity others not. Therefore the value can nicely be treated asMaybe<string>
.
interfaceClient{name:stringsecondName:Maybe<string>lastName:string}
The approach will enable implementation of client's formatting as a string the following way.
classVIPClient{// some implementationtoString(){return"VIP:"+this.name+// returns second name surrounded// by spaces or just a spacethis.secondName.then(secondName=>`${secondName} `).getOrElse("")+this.lastName}}
Computations which might fail due to obvious reason are also a good application forMaybe
. Lowest common denominator might be unavailable. That is why the signature makes perfect sense forgetLCD()
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 byFailure
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 asSuccess
or “Data is not initialized” error message asFailure
.
classResultExample{value:Result<Value,string>=Failure(“Dataisnotinitialized”)init(value:Value){this.value=Success(value)}calculateSomethingBasedOnValue(){returnthis.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 utilizesResult
as a return type.
parseUser(str:string):Result<Data>
The output of such a function might contain processed value asSuccess
or an explanation of an error asFailure
.
Conclusion
Promise
,Result
andMaybe
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)
For further actions, you may consider blocking this person and/orreporting abuse