DEV Community

Cover image for Promise and other tools for Monadic error handling
Alexey Tukalo
Alexey Tukalo

Posted on

Promise and other tools for Monadic error handling

Today I would like to talk with you about one of the most common utilization for monads - Monadic error handling. For those, who doesn't have experience with a concept of monads, you are welcome to check out my attempt to make a brief introduction to the topic in a previous part of the series.

I Promise you

Usually, a useful program involves interactions with some remote services, databases or local file storage. There number of reasons for it:

  • A prior state of the application should be loaded during initialization
  • Data changes should be synced during an application execution
  • State of an app should be saved on closing phases

This kind of requests can be imagined as a sailing ship sent in a dangerous oversee expedition to transport a valuable resource. Such an adventure might take an arbitrary amount of time. Moreover, it is a risky business. Hence the cargo can be lost in the process. Therefore, the abstraction suitable for modelling operation of this kind should be able to deal with a delay of unknown duration as well as with possible failure.

This type of work is called asynchronous computation. A special kind of Monad commonly represents it. The abstraction with slight variations was implemented under different names in many environments. It is known as Promise, Future, Task and so on. The most widely used one is JavaScript's take on it, which is going to be used as a primary example for the article. Promise wraps around the computation. It allows a developer to attach callbacks capable of handling the result of the computation. The function is applied to the attached value as soon as it became available. Following listening presents signature of a JavaScript function responsible for the handling of HTTP/HTTPS requests in an environment of a modern browser.

function fetch(
    url: RequestInfo,
    init?: RequestInit
): Promise<Response>;
Enter fullscreen mode Exit fullscreen mode

It consumes two arguments describing a request and returns a result inclosed inside of Promise that indicates the asynchronous nature of the operation. Result of the operation can be accessed via callback passed to Promise.prototype.then() function.

const rawResult = fetch(someURL)
const processedResult = rawResult
    .then( value => {
        // some processing
        const processedValue = processValue(value)
        return value
    })
Enter fullscreen mode Exit fullscreen mode

The applications of callbacks can easily be chained.

rawResult
    .then( value => {
        // some processing stage 1
        const processedValue = processValue1(value)
        return value
    })
    .then(value => {
        // some processing stage 2
        const processedValue = processValue2(value)
        return value
    )
Enter fullscreen mode Exit fullscreen mode

It is useful in a situation, then the result has to be preprocessed and passed father.

function getData() {
    return fetch(someURL)
        .then( value => {
            // some processing stage 1
            const processedValue = processValue1(value)
            return value
        })
}   
const result = getData()
    .then(value => {
        // some processing stage 2
        const processedValue = processValue2(value)
        return value
    )
Enter fullscreen mode Exit fullscreen mode

Another remarkably helpful property of then() is its ability to chain functions which returns Promise that occurs in a situation of subsequent requests. For example, there are two functions. One of them can get some basic information about the user by his/her name. The information is repressed as an object of User type. The second function uses part of the information to request pictures of the user.

// function capable of getting required data
function getUser( name: string ): Promise<User> {
    return fetch(`${someURL}/${name}`)
        .then(parseUser)
}  
function getUserPic( user: User ) {
    return fetch(`${someURL}/${user.picURL}`)
}
Enter fullscreen mode Exit fullscreen mode

The functions can be utilized in the following way to retreat a user's image by name.

// utilization of the functions
const picPromise = getUser('Alex T')
    .then( user => {
        return getUserPic(user)
    )
picPromise.then( pic => {
    // handle the picture
})
Enter fullscreen mode Exit fullscreen mode

As it was previously mentioned Promise is also capable of modelling errors. An error is dispatched to a callback passed to Promise.prototype.catch() function or as the second argument to Promise.prototype.then(). It can handle the problem by returning a fallback value.

result
    .catch( err => {
        // error preprocessing 
    })
    .catch( err => {
        // error handling
        return fallBackValue
    })
Enter fullscreen mode Exit fullscreen mode

Monads for error handling

As it was previously shown Promise could model asynchronous computations as well as computations that might fail. The coupling is not entirely necessary hence Rust's implementation of similar abstractions is only responsible for the handling of asynchronous operations. On the other hand, it might also be useful to have monadic abstractions purely dedicated to the representation of possibly failing computations or missing data. The differentiation between three of the cases might help to communicate an intention behind code in a more clear way. Getting rid of asynchrony allows having optimized lightweight implementation and extended specialized APIs.

This approach of error handling is an alternative to exceptions. Several relatively recent languages chose the approach like Scala, Haskell, Rust, Elm, Clojure and many others. The main advantage of such a solution is explicitness. Possibility of failure is clearly indicated in the signature of each particular function; hence data can not be directly accessed. Therefore, a developer is forced to deal with it in a way that error handling became an integral part of the code.

So, this approach might be preferable in a situation where code is meant to fail. For example, the application might rely on frequently incorrect input like in case of compilers or other untrusty data sources. Have you ever heard about outstanding error messages produced by compilers of Rust and Elm?

Conclusion

Monads can be used for modelling of computation with high latency or possibilities of failure. Depending on the implementation, the abstraction can handle both of the situations or focus on just one of them. In the following article, we will take a look at examples of Monads specialised on error handling.

Disclaimer

Promise doesn't strictly follow the mathematical definition of Monad. The implementation has some minor limitations, which are neglectable in most of production use cases. Moreover, it works as a perfect example of a mechanic useful for a wide audience.

Discussion (0)