DEV Community

Angelo
Angelo

Posted on

Using Futures to handle complex asynchronous operations in javascript.

To demonstrate Futures in javascript I will be referencing the Fluture library. A Fantasy Land compliant Monadic alternative to Promises.

Fluture offers a control structure similar to Promises.
Much like Promises, Futures represent the value arising from the success or failure of an asynchronous operation (I/O).

Getting a value from an end point using a promise is a fairly trivial operation.

It may look something like this.

import axios from "axios";

var getToDo = id => axios.get(`https://jsonplaceholder.typicode.com/todos/${id}`)

getToDo(1)
 .then(({data}) => data)
 .catch(e => e)

// { userId: 1, id: 1, title: 'delectus autautem', completed: false }
Enter fullscreen mode Exit fullscreen mode

Getting a value out from an end point using a future is also fairly trivial. It looks like this.

import axios from "axios";
import { tryP } from "fluture";

var getToDo = id => 
 tryP(() => axios.get(`https://jsonplaceholder.typicode.com/todos/${id}`))

getToDo(1).fork(err => err, ({ data }) => data)

// { userId: 1, id: 1, title: 'delectus autautem', completed: false }
Enter fullscreen mode Exit fullscreen mode

Something to note. To get the result of a Future, we must fork. The Left side of our fork will run if there is an error, similar to catch. The right side of our fork will contain our result, similar to then.

Futures allow us to chain and map their results into other futures or perform data manipulation on the results of a future before returning, as well as catching errors and managing those before actually forking.

Here is an example.

import { tryP, of, ap } from "fluture";
import axios from "axios";

const loginRequest = email => password =>
  tryP(() => 
   axios({
    url :`https://www.fake.com/login`, 
    data : { email, password }
   })
)

const userDetailsRequest = id =>
  tryP(() => axios.get(`https://www.fake.com/userDetails/${id}`))


const login = email => password => loginRequest(email)(password)
 .chain({ data }) =>  userDetailsRequest(data.id))
 .map(({ data }) => formatData(data))
 .mapRej(err => formatError(err))


login('faker@gmail.com')('admin123').fork(err => err, userDetails => userDetails)

Enter fullscreen mode Exit fullscreen mode

Difference between .chain .map .mapRej and .chainRej

  1. Chain: the result of a .chain must be a Future
  2. Map: the result of a .map is not a future
  3. MapRej: the result of a .mapRej is not a future and will only be triggered if a Future fails
  4. ChainRej: the result of a .chainRej must be a future and will only be triggered if a Future fails

If a future fails/errors, it will "short circuit" .map and .chain will not be run, the flow will be directed to either . mapRej or .chainRej whichever is defined by the programmer.

Now to something a little more complex.

I was recently asked to write a program that fetched comments for a blog post. There was a request which returned the blog post and it included an array of id's. Each id represented a comment. Each comment required its own request.

So imagine having to make 100 requests to get back 100 comments.

(Parallel)[https://github.com/fluture-js/Fluture/tree/11.x#parallel]

Fluture has an api called parallel

Parallel allows us to make multiple async requests at once, have them resolve in no particular order, and returns us the results once all requests have completed.

Here is what this would look like.

import { tryP, parallel } from "fluture";
import axios from "axios";

// Our Future
const getCommentRequest = comment_id =>
  tryP(() => axios.get(`https://www.fake-comments.com/id/${comment_id}`))
  .map(({ data }) => data);

// comments is an array of ID's
const getComments = comments => 
 parallel(Infinity, comments.map(getCommentRequest))

// Infinity will allow any number of requests to be fired simultaneously, returning us the results once all requests have completed.

// The result here will be an array containing the response from each request.
getComments.fork(err => err, comments => comments)


Enter fullscreen mode Exit fullscreen mode

Replacing Infinity with a number. Say 10, would fetch 10 comments at a time, resolving once all comments in the array had been retrieved.

In the next Example, imagine a case where may must fetch some data which is only useful to us if some initial request(s) succeeds.

(AP)[https://github.com/fluture-js/Fluture/tree/11.x#ap]

Applies the function contained in the left-hand Future to the value contained in the right-hand Future. If one of the Futures rejects the resulting Future will also be rejected.

Lets say we need to fetch a users account. If the account is found, we can then attempt to fetch their friends. If we find their friends, we can attempt to fetch their friend's photos. If any of these requests fail, them the whole flow short circuits, and we would fall in left side of our fork where we can handle the error.

import { tryP, of, ap } from "fluture";
import axios from "axios";

// Our Futures
const retrieveUserAccount = id =>
  tryP(() => axios.get(`https://www.fake.com/user/${id}`))

const retrieveUserFriends = id =>
  tryP(() => axios.get(`https://www.fake.com/friends/${id}`))

const retrieveUserFriendsPhotos = id =>
  tryP(() => axios.get(`https://www.fake.com/friendsPhotos/${id}`))

const retrieveUserInformation = id =>
  of(account => 
      friends => 
       friendsPhotos => {account, friends, friendsPhotos}) //All results returned
    .ap(retrieveUserFriendsPhotos(id)) // 3rd
    .ap(retrieveUserFriends(id)) // 2nd
    .ap(retrieveUserAccount(id)) // Fired 1st

retrieveUserInformation.fork(err => err, results => results)

Enter fullscreen mode Exit fullscreen mode

Futures allow us to nicely compose our asynchronous operations.

(More information on Flutures)[https://github.com/fluture-js/Fluture/tree/11.x]

Thanks for reading!

Discussion (0)