The Problem
1
2
3
4
5
6
7
8
9
Write JavaScript code using the setTimeout function to print 3 lines asynchronously.
The output should do the following:
1. Wait 2 seconds
2. Print out “First task done!”
3. Wait another 2 seconds
4. Print out “Second task done!”
5. Wait another 2 seconds
6. Print out “Third task done!”
So, I was given this problem a little while ago. It’s a deceptively simple problem (for beginners), and I think it gets at core of a common design problem.
Naive Solution
In my head, setTimeout
worked like this: Give it some code and a time to wait, and it’ll run the code after said time has passed.
After thinking it over for a bit, this was the first thing that popped in my head:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// code to run
function firstTask() {
console.log("First task done!")
}
function secondTask() {
console.log("Second task done!")
}
function thirdTask() {
console.log("Third task done!")
}
// time to wait
setTimeout(firstTask, 2000)
setTimeout(secondTask, 4000)
setTimeout(thirdTask, 6000)
Since secondTask
runs two seconds after firstTask
, that means it occurs four seconds after running the program!!! :DDD
Yeah, no.
Though it technically works, it doesn’t solve the problem at all: the second task doesn’t run two seconds after the first one, it just so happens to. If I wanted to make secondTask
run three seconds after firstTask
instead of two, I’d have to do this:
1
2
3
setTimeout(firstTask, 2000)
setTimeout(secondTask, 5000) // modified
setTimeout(thirdTask, 7000) // ALSO modified!
So, no good. What I really needed to do was make each task run after the previous one.
Callback Chaining
Okay, secondTask
runs after firstTask
right?
1
2
3
4
function firstTask() {
console.log("First task done!")
setTimeout(secondTask, 2000)
}
Wait.
If I just wanted to run firstTask
, it would also run secondTask
. I need the sequencing to happen outside of either task.
1
2
3
4
setTimeout(() => {
firstTask()
setTimeout(secondTask, 2000)
}, 2000)
Adding thirdTask
in…
1
2
3
4
5
6
7
setTimeout(() => {
firstTask()
setTimeout(() => {
secondTask()
setTimeout(thirdTask, 2000)
}, 2000)
}, 2000)
Now, when I change secondTask
to three seconds I only do this:
1
2
3
4
5
6
7
setTimeout(() => {
firstTask()
setTimeout(() => {
secondTask()
setTimeout(thirdTask, 2000)
}, 3000) // modified
}, 2000)
…And it works!
This solved my previous problem, but I still wasn’t completely happy with it.
For one, it’s hard to read. Every other line is stuffed with boilerplate. It’s clearer when you extend the pattern out like this:
1
2
3
4
5
6
7
8
9
setTimeout(() => { // boilerplate
firstTask()
setTimeout(() => { // boilerplate
secondTask()
setTimeout(() => { // boilerplate
thirdTask()
}, 2000)
}, 2000)
}, 2000)
From here, it’s easy to see the problem. I’ve got some repetitive code using setTimeout
that I have to write before every task.
Contrast that with the naive solution from earlier:
1
2
3
setTimeout(firstTask, 2000)
setTimeout(secondTask, 4000)
setTimeout(thirdTask, 6000)
It’d be nice if I could chain these tasks together without all the boilerplate.
But, that’s not possible, is it?
Pipelines
Introducing the JS
Promise
– your one-stop shop to call-back for all your callback chaining needs! Call now, and it’ll call back as many times as you need!
Allow me to demonstrate.
Recipe: How to Cook a Call-Forward
Ingredients
- A stack of boilerplates (covered in callbacks)
- A properly prepared promise
Steps
1. Set your boilerplates down on the table
1
2
3
4
5
6
7
8
9
setTimeout(() => { // boilerplate
firstTask()
setTimeout(() => { // boilerplate
secondTask()
setTimeout(() => { // boilerplate
thirdTask()
}, 2000)
}, 2000)
}, 2000)
2. Flatten them out, one by one
1
2
3
4
5
6
7
8
9
setTimeout(() => { // boilerplate
firstTask()
}, 2000)
setTimeout(() => { // boilerplate
secondTask()
}, 2000)
setTimeout(() => { // boilerplate
thirdTask()
}, 2000)
3. Promise each one you’ll have the resolve to call-back
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
new Promise((resolve, reject) => {
setTimeout(() => { // boilerplate
resolve(firstTask())
}, 2000)
})
new Promise((resolve, reject) => {
setTimeout(() => { // boilerplate
resolve(secondTask())
}, 2000)
})
new Promise((resolve, reject) => {
setTimeout(() => { // boilerplate
resolve(thirdTask())
}, 2000)
})
4. Then enjoy your newly cooked call-forward
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
new Promise((resolve, reject) => {
setTimeout(() => { // boilerplate
resolve(firstTask())
}, 2000)
})
.then(() =>
new Promise((resolve, reject) => {
setTimeout(() => { // boilerplate
resolve(secondTask())
}, 2000)
})
)
.then(() =>
new Promise((resolve, reject) => {
setTimeout(() => { // boilerplate
resolve(thirdTask())
}, 2000)
})
)
Refactoring
Jokes aside, it’s now much easier to fix the boilerplate. Instead of writing each promise by hand, let’s put it in a function!
1
2
3
4
5
6
7
function setTimeoutPromise(func, ms) {
return new Promise((resolve, reject) => {
setTimeout(() => { // boilerplate (now in one place)
resolve(func())
}, ms)
})
}
Now my call-forward *cough* pipeline *cough* looks like this:
1
2
3
setTimeoutPromise(firstTask, 2000)
.then(() => setTimeoutPromise(secondTask, 2000))
.then(() => setTimeoutPromise(thirdTask, 2000))
Modifying secondTask
is easy:
1
2
3
setTimeoutPromise(firstTask, 2000)
.then(() => setTimeoutPromise(secondTask, 3000)) // modified
.then(() => setTimeoutPromise(thirdTask, 2000))
And adding fourthTask
is too:
1
2
3
4
setTimeoutPromise(firstTask, 2000)
.then(() => setTimeoutPromise(secondTask, 3000))
.then(() => setTimeoutPromise(thirdTask, 2000))
.then(() => setTimeoutPromise(fourthTask, 2000)) // modified
The Solution
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function firstTask() {
console.log("Completed the first task")
}
function secondTask() {
console.log("Completed the second task")
}
function thirdTask() {
console.log("Completed the third task")
}
function setTimeoutPromise(func, ms) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(func())
}, ms)
})
}
setTimeoutPromise(firstTask, 2000)
.then(() => setTimeoutPromise(secondTask, 2000))
.then(() => setTimeoutPromise(thirdTask, 2000))
My final solution. Beatiful, isn’t it?
So, yeah. Pipelines. Use them when you need to chain anything.
…
Wait, you still don’t understand Promises?
A Promise is just a monoid in the category of callbacks, what’s the problem?