Difference between a Promise and a Task

Once you have a Promise instance the action has already started. Task instance does not run until someone calls .fork()

If you program in modern JavaScript, you have probably replaced all your callbacks with Promises. Wrapping your mind around Promises requires some practice and even then there might be traps. I wrote a lot of blog posts about Promises, mostly to remind myself how to do certain actions using them.

Recently I have been watching the excellent (and hilarious) videos "Classroom Coding with Prof. Frisby" that show how to use functional JavaScript in the real world. Here are the links to part 1, part 2, and part 3. I have noticed how Professor Frisby is using data.Task to compose asynchronous functions that either resolve with a value, or reject with an error. For example, here is how the front end code is downloading or uploading data

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const {getJSON, post} = require('jquery')
const Task = require('data.task')
const Http = {
// get :: Url -> Task Error JSON
get: url => {
return new Task((rej, res) => getJSON(url).error(rej).done(res))
},
// post :: Url, JSON -> Task Error JSON
post: (url, params) => {
return new Task((rej, res) =>
post(url, JSON.stringify(params)).error(rej).done(res)
)
},
}

In this situation, Task (from data.task) looks a lot like a Promise, doesn't it? What's the difference? It cannot be that Task is used here to nicely keep track of the possible error, I use Promises for error values the same way. The order of arguments is different: Task expects the error handling callback at first position, while Promise puts it at the second one. But that is a tiny difference.

What is really different is when an operation runs. Let us compare both solutions on a simple example. First, a timeout using a Task

1
2
3
4
5
6
7
8
9
10
11
12
13
const Task = require('data.task')
function getTask() {
console.log('returning new task')
return new Task(function (reject, resolve) {
console.log('setting task timeout')
setTimeout(function () {
console.log('Task has finished')
resolve()
}, 100)
})
}
var task = getTask()
console.log('made task')

The code has lots of log statements for clarity. Let us run it

$ node index.js
returning new task
made task

Hmm, the code never set the timeout, and has never executed the timeout callback!

Let us replace Task with Promise and do the same.

1
2
3
4
5
6
7
8
9
10
11
12
function getPromise() {
console.log('returning new promise')
return new Promise(function (resolve, reject) {
console.log('setting promise timeout')
setTimeout(function () {
console.log('Promise has finished')
resolve()
}, 100)
})
}
var promise = getPromise()
console.log('made promise')

Same code using ES6 Promise implementation from Node 4.2.2

$ node index.js
returning new promise
setting promise timeout
made promise
Promise has finished

Interesting, the entire chain of actions fired right away. As soon as one has created a Promise object, the callback function passed to its constructor is executed, which sets the timeout, etc. Which means that IF you have an instance of the Promise - the action has already started. It might have already finished! For example

1
2
3
4
5
6
function getPromise() {
console.log('returning new promise')
return Promise.resolve(42)
}
var promise = getPromise()
console.log('made promise', promise)
$ node index.js
returning new promise
made promise Promise { 42 }

Even better to copy / paste this code into Chrome or any other modern browser that clearly shows the Promise state. The above code shows

returning new promise
made promise Promise {[[PromiseStatus]]: "resolved", [[PromiseValue]]: 42}

When does Task execute its callback function? Only when someone calls task.fork.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const Task = require('data.task')
function getTask() {
console.log('returning new task')
return new Task(function (reject, resolve) {
console.log('setting task timeout')
setTimeout(function () {
console.log('Task has finished')
resolve()
}, 100)
})
}
var task = getTask()
console.log('made task')
task.fork(console.error, console.log)
$ node index.js
returning new task
made task
setting task timeout
Task has finished

The deferred execution has nice benefits. For example, you can compose tasks before running them. You just need to do it yourself.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const Task = require('data.task')
function getTask() {
console.log('returning new task')
return new Task(function (reject, resolve) {
console.log('setting task timeout')
setTimeout(function () {
console.log('Task has finished')
resolve()
}, 100)
})
}
var task1 = getTask()
var task2 = getTask()
var combinedTask = task1.chain(() => task2)
console.log('made tasks')
combinedTask.fork(console.error, console.log)
$ node index.js
returning new task
returning new task
made tasks
setting task timeout
Task has finished
setting task timeout
Task has finished

We used Task.chain method from the first task to return the second task, which implicitly calls task2.fork method.

Just like Promise.resolve is a shortcut for creating a function that resolves with a given value, Task.of can be used to start with a value.

1
2
3
4
const Task = require('data.task')
var task1 = Task.of(21)
console.log('made task')
task1.fork(console.error, console.log)
$ node index.js
made task
21

One can even use Task.map to quickly pass the returned value through a composition of callback functions.

1
2
3
4
5
const Task = require('data.task')
var task1 = Task.of(21)
console.log('made task')
var task2 = task1.map(x => x * 2).map(x => x - 1)
task2.fork(console.error, console.log)
$ node index.js
made task
41

Task is an excellent way to combine functions (async or not) first, before starting executing them. This allows me to prepare for any possible side effects. For example, if I need to test a DB query, it is very problematic with Promises. The mock DB has to be started before any code using DB runs.

db-connection.js
1
2
3
4
var db = require('db')
var connection = db.connect() // returns a Promise
// the code already tries to connect!
module.exports = connection

During unit testing we must run the mock DB code before any code tries to load db-connection.js otherwise it fails. But if we use Tasks, we can write both production and testing code simply like this

1
2
3
4
5
6
7
8
// db-connection.js
var db = require('db')
var Task = require('data.task')
var connection = new Task(function (reject, resolve) {
// db.connect returns a Promise
db.connect().then(resolve, reject)
})
module.exports = connection // a Task
1
2
3
4
5
// query.js (production code)
var connection = require('./db-connection')
var query = ...
connection.fork(onError, query)
// the code tries to connect now
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// query-spec.js (test code)
var connection = require('./db-connection')
var mockDB = setupMockDB()
describe('query', () => {
beforeAll(setupMockDB)
it('runs query', (done) => {
connection.fork(testFails, () => {
// the code tries to connect now
// but hits the mock DB
// runMockQuery returns a Promise
runMockQuery.catch(testFails).finally(done)
})
})
})

I hope to start using the Task monad in my code, it certainly seems to help control the code execution.

Relevant

data.task source, part of Folktale suite of JavaScript libraries generic functional programming.

Another good tutorial showning Tasks in action is The Little Idea of Functional Programming by Jack Hsu