DEV Community

loading...

Other tools for Monadic error handling

airtucha profile image Alexey Tukalo ・6 min read

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.") )
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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>()
    )
Enter fullscreen mode Exit fullscreen mode

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...
}
Enter fullscreen mode Exit fullscreen mode
// 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...
}
Enter fullscreen mode Exit fullscreen mode

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)
        )
Enter fullscreen mode Exit fullscreen mode
// 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"
    )
Enter fullscreen mode Exit fullscreen mode

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...
}
Enter fullscreen mode Exit fullscreen mode
// 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...
}
Enter fullscreen mode Exit fullscreen mode

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>
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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)
     )
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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".

Discussion (0)

pic
Editor guide