Slides for Henning's talk on Sep 21st 2017.
print('script start')
html = get('/foo')
print(html)
print('script end')
Script outputs 'script start'
, (long delay), '<html>...</html>'
, 'script end'
.
print('script start')
get('foo', done: function(html) {
print(html)
})
print('script end')
Script outputs 'script start'
, 'script end'
, (long delay), '<html>...</html>'
.
This is the model of Ruby, Java, Elixir, PHP, Python, ...
This is the model of many JavaScript APIs, EventMachine (Ruby), Netty (Java), LibEvent (C), Twisted (Python)
Math.pow(3) // sync API! Blocks and returns 9
$.ajax('/foo', done: function() { ... }) // async API! Returns at once and runs callbacks later
Most current and future HTML5 browser APIs have async signatures.
We want to write a function that:
<div class="users">...</div>
/users
to fetch new HTML<div>
with the fetched HTML<div>
.function refreshUsers(callbacks) {
$('.users').animate('fade-out', {
done: function() {
$.ajax('/users', {
done: function(html) {
$('.users').replaceWith(html)
$('.users').animate('fade-in', {
done: callbacks.done
})
}
})
}
})
}
refreshUsers({
done: function() {
console.log('Users refreshed!')
}
);
function refreshUsers(callbacks) {
$('.users').animate('fade-out', {
done: function() {
$.ajax('/users', {
done: function(html) {
$('.users').replaceWith(html)
$('.users').animate('fade-in', {
done: callbacks.done,
error: callbacks.error
})
},
error: callbacks.error
})
},
error: callbacks.error
})
}
refreshUsers({
done: function() { console.log('Users refreshed!') },
error: function() { console.log('Users error') }
})
function refreshUsers() {
var promise = $('.users').animate('fade-out')
promise = promise.then(function() {
return $.ajax('/users')
})
promise = promise.then(function(html) {
$('.users').replaceWith(html)
return $('.users').animate('fade-in')
})
return promise
}
refreshUsers().then(
function() { console.log('Users refreshed!') },
function() { console.log('Users error') }
)
refreshUsers()
takes no callbackYou will often hear the verb "resolve", which is not a synonym for "settle". It means "start the resolution procedure" which will eventually either fulfill or reject the promise. More on that later.
promise = new Promise(function(resolve, reject) {
// do long-running job and eventually
// call resolve(value) or reject(reason)
})
promise.then(onFulfilled, onRejected)
JavaScript has an async function setTimeout
that calls a given function after a given number of milliseconds:
setTimeout(function() {
console.log('200 ms have passed!')
}, 200)
Let's write a function whenTimeout
that returns a promise when the time has passed:
whenTimeout(200).then(
function() { console.log('200 ms have passed!') }
)
function whenTimeout(millis) {
return new Promise(function(resolve, reject) {
setTimeout(function() { resolve() }, millis);
})
}
Now let's extend whenTimeout
so it rejects if the given time is negative:
whenTimeout(200).then(
function() { console.log('200 ms have passed!') },
function(reason) { console.error('Something went wrong: %o', reason) }
)
function whenTimeout(millis) {
return new Promise(function(resolve, reject) {
if (millis < 0) {
reject('Please pass a positive number')
}
setTimeout(function() { resolve() }, millis);
})
}
Each call to then()
returns a new promise:
fun1().then(fun2).then(fun3).then(fun4)
| | | |
Promise | | |
Promise | |
Promise |
Promise
Your functions no longer need to manage what happens after:
function fadeOut() {
return $('.users').animate('fade-out').promise()
}
function requestUsers() {
return $.ajax('/users')
}
function updateUsers(html) {
$('.users').replaceWith(html)
return $('.users').animate('fade-in').promise()
}
Functions can be freely composed in an async control flow:
fadeOut().then(requestUsers).then(updateUsers)
Promise.resolve(4)
This is the same as:
new Promise(function(resolve, reject) { resolve(4) })
Promise.reject('something went wrong')
This is the same as:
new Promise(function(resolve, reject) { reject('something went wrong') })
allDone = Promise.all([fetchUsers(), fetchPosts()])
allDone.then(function(users, posts) { ... })
Many people use "resolve" as a synonym for "fulfill", but it really isn't.
Calling resolve()
with a value fulfills the promise with that value:
promise = new Promise(function(resolve, reject) {
resolve(3) // fulfills promise with the value 3
})
promise.then(function(x) {
// x is now 3
})
Calling resolve()
with another promise adopts the state of that promise:
var otherPromise = Promise.resolve(3)
promise = new Promise(function(resolve, reject) {
// adopt the state of otherPromise
resolve(otherPromise)
})
promise.then(function(x) {
// x is now 3
})
This means resolve()
can reject your promise!
var otherPromise = Promise.reject(3)
promise = new Promise(function(resolve, reject) {
// adopt the state of otherPromise
resolve(otherPromise)
})
promise.then(function(x) {
// this is never called
})
promise.catch(function(x) {
// x is now 3
})
Each then()
callback gets the settlement value of the previous callback:
promise = Promise.resolve(1)
promise = promise.then(x) {
// x is now 1
return 2
}
promise = promise.then(y) {
// y is now 2
return 3
}
promise = promise.then(z) {
// z is now 3
}
If a then()
callback returns a promise, the chain waits until that returned promise is settled:
promise = Promise.resolve(1)
promise = promise.then(x) {
// x is now 1
return whenTimeout(1000)
}
promise = promise.then() {
// 1000 milliseconds have passed
return 2
}
promise = promise.then(y) {
// y is now 2
}
Returning a promise is a way to delay further resolution. E.g. when a promise callback discovers it needs to do more async work:
function fetchUserAndOptionalAvatar() {
return fetchUser().then(user) {
if (user.avatar) {
return fetchAvatar()
} else {
return user
}
}
}
You can only settle a promise once. Thus handlers are only called once. So do not use promises for event handlers:
whenClicked().then(function() {
// bad! this will only run once
})
Use vanilla callbacks or observables for event handlers:
whenClicked(function() {
// vanilla function can be called often
})
then()
It does not matter if you register then(handler)
before or after the promise is resolved.
Registering a handler on a settled promise will call the handler.
then()
is an async function. This means that, even if your promise is already settled, a new then(handler)
will not be called in the current execution frame. It will be called in the next microtask instead:
console.log('script start')
fulfilledPromise = Promise.resolve(3)
fulfilledPromise.then(function() {
console.log('promise handler')
})
console.log('script stop')
This prints 'script start'
, 'script stop'
, 'promise handler'
.
More on microtasks later!
We already learned that each then()
returns a new Promise:
fun1().then(fun2).then(fun3).then(fun4)
| | | |
Promise | | |
Promise | |
Promise |
Promise
If fun1()
fulfills but fun2()
rejects, all subsequent chain links reject:
fun1().then(fun2).then(fun3).then(fun4)
| | | |
fulfilled | | |
rejected | |
rejected |
rejected
In the example above, the callbacks fun3
and fun4
will not be called.
Don't do this:
function whenTimeout(millis) {
return new Promise(function(resolve, reject) {
if (millis < 0) {
throw('Please pass a positive number')
}
setTimeout(resolve, millis);
})
}
You are forcing callers to handle both sync and async errors:
try {
whenTimeout(200).then(
function() { console.log('200 ms have passed!') }
function(reason) { console.error('Something went wrong: %o', reason) }
)
} catch (reason) {
console.error('Something went wrong: %o', reason)
}
Async function signal failure by returning a rejected promise:
function whenTimeout(millis) {
return new Promise(function(resolve, reject) {
if (millis < 0) {
reject('Please pass a positive number')
}
setTimeout(resolve, millis);
})
}
This way callers only need to handle async errors:
whenTimeout(200).then(
function() { console.log('200 ms have passed!') }
function(reason) { console.error('Something went wrong: %o', reason) }
)
This will become more important when we talk about async/await later.
then()
handlersfunction fetchUsers() {
return Promise.resolve([])
}
promise = fetchUsers().then(function(users) {
if (users.length) {
...
} else {
throw "no users found"
}
})
promise.catch(function(reason) { console.error(reason) })
If fetchUsers()
fulfills with an empty Array, promise
is now rejected with "no users found".
If you have a large blob of sync code and need to convert it to an async API, a hack is to put it all into the then()
handler of a resolved promise. This way thrown errors will automatically be converted to rejected promise:
function whenTimeout(millis) {
return Promise.resolve().then(function() {
if (millis < 0) {
throw('Please pass a positive number') // will reject the whenTimeout() promise
}
return new Promise(function(resolve, reject) {
setTimeout(resolve, millis);
})
});
}
We know we can pass an onRejected
handler:
promise.then(onFulfilled, onRejected) // returns new promise
But examples often only pass an onFulfilled
handler. So when do we need to pass onRejected
?
You can omit it only if your then()
value is returned because the promise returned by promise1.then
therefore adopts the state of promise2
:
promise1 = new Promise(...)
promise2 = new Promise(...)
handleError = function(e) { console.error(e) }
promise1.then(
function(x) {
return promise2.then(onFulfilled)
}
).catch(handleError) // will handle rejections for both promise1 and promise2
This is the equivalent of unhandled exceptions bubbling up in sync control flow.
Here is a bad example where we omit the onRejected
handler, but don't return the resulting promise:
promise1 = new Promise(...)
promise2 = new Promise(...)
handleError = function(e) { console.error(e) }
promise1.then(
function(x) {
promise2.then(onFulfilled) // nothing will handle rejection of promise2
return 'value'
}
).catch(handleError) // will handle rejections for promise1 only
This is the equivalent of an empty catch {}
clause that silently eats exceptions.
If you do this, Chrome will print an error to the console, informing you that you didn't handle a rejection:
Unhandled promise rejection Promise { ... }
With promises you can do anything that you can do with sync APIs.
It is useful to be able to translate basic control flows from sync to async style.
function foo() {
return 'foo'
}
function foo() {
return Promise.resolve('foo')
}
function foo() {
throw 'foo'
}
function foo() {
return Promise.reject('foo')
}
Note that we don't throw the rejected promise. We just return it.
foo()
bar()
foo().then(bar)
x = foo()
bar(x)
foo().then(bar)
Or more verbose:
foo().then(function(x) { bar(x) })
x = foo()
if (x > 0) {
return x * x
} else {
throw "Bad value"
}
foo().then(function(x) {
if (x > 0) {
return Promise.resolve(x * x)
} else {
return Promise.reject("Bad value")
}
})
try {
x = foo()
return x * x
} catch (e) {
return -1
}
foo().then(
function(x) { return x * x },
function(e) { return Promise.resolve(-1) }
)
Or you can also write:
foo()
.then(function(x) { return x * x })
.catch(function(e) { return Promise.resolve(-1) })
promise.catch(fn)
is shorthand for promise.then(null, fn)
.
async
/await
is syntactic sugar that lets you write async code in a sync style.
You know this from jQuery:
$.ajax('/users').then(
function(html) { console.log(html) }
})
With await
we can write the same code like this:
html = await $.ajax('/users')
console.log(html)
Your transpiler will convert this into vanilla JavaScript using promises or generators.
async
/await
is available in Babel and CoffeeScript 2 (which needs Babel).
function throwOnMonday() {
if (new Date().getDay() == 1) {
return Promise.reject("oh no, it's monday!")
} else {
return Promise.resolve("all good")
}
}
With async
our function will convert its return value into a fulfilled promise:
async function throwOnMonday() {
if (new Date().getDay() == 1) {
throw "oh no, it's monday"
} else {
return "all good"
}
}
Sync control flow
function f1() { return 2 }
function f2(x) { return x * x }
try {
var x = f1()
var y = f2(x)
console.log("y is %o", y)
} catch (e) {
handleError(e)
}
If f1()
and f2()
have an async API, our control flow must become async:
function f1() { return Promise.resolve(2) }
function f2(x) { return Promise.resolve(x * x) }
promise = f1()
promise = promise.then(
function(x) { return f2(x) }
)
promise = promise.then(
function(y) { console.log("y is %o", y) },
function(e) { handleError(e) }
)
With async
/await
we can pretend f1()
and f2()
are sync again:
async function f1() { return 2 }
async function f2(x) { return x * x }
try {
var x = await f1()
var y = await f2(x)
console.log("y is %o", y)
} catch (e) {
handleError(e)
}
async function refreshUsers() {
await $('.users').animate('fade-out')
var html = await $.ajax('/users')
$('.users').replaceWith(html)
await $('.users').animate('fade-in')
}
await refreshUsers()
console.log('Users refreshed!')
The error from throwError()
below will never be caught, because asyncError()
does not really throw an error. It returns a rejected promise!
async function asyncError() {
throw new Error('will never be caught')
}
async function foo() {
try {
return asyncError()
} catch(e) {
console.error(e)
}
}
You need to be super careful when you want to convert from async style. If you want to handle async calls in a sync call, you must use await
:
Here is the fixed code:
async function asyncError() {
throw new Error('will never be caught')
}
async function foo() {
try {
return await asyncError() // additional await here
} catch(e) {
console.error(e)
}
}
Making every function async is the safest bet if you need to keep your API stable. Almost every modern HTML5 browser API is async for that reason.
However, async functions are also much more painful to use than sync functions. This is super convenient:
array1 = []
array1.push('a')
array1.push('b')
array2 = []
array2.push('c')
array2.push('d')
array3 = array1.concat(array2)
console.log(array3)
Compare this to an async version of the same:
whenArray1Done = newArray().then(array) {
return array.push('a')
}.then(array) {
return array.push('b')
}
whenArray2Done = newArray().then(array) {
return array.push('b')
}.then(array) {
return array.push('c')
}
Promise.all([whenArray1Done, whenArray2Done]).then(function(array1, array2) {
return array1.concat(array2)
}).then(function(array3) {
console.log(array3)
})
Great post by Bob Nystrom: What color is your function Show archive.org snapshot
You have probably used setTimeout(0, fn)
to push work into the "next frame".
setTimeout(0)
pushes work into the next task, which roughly means "after the browser had a chance to render the results of this frame and JavaScript regains control".
JavaScript task
Render
JavaScript task
Render
JavaScript task
Render
Pushing a task is super expensive because the browser might render before your task gets called. It's also very easy to run into timeout clamping Show archive.org snapshot when a callback pushes a callback.
Promise callbacks are run in the next microtask instead. This is a separate queue for lighter tasks:
JavaScript task
JavaScript microtask
Render
JavaScript task
JavaScript microtask
JavaScript microtask
JavaScript microtask
JavaScript microtask
Render
JavaScript task
Render
Mutation Observers, postMessage
, MessageChannel
all use the microtask queue.
Great visualization by Jake Archibald: Tasks, microtasks, queues and schedules Show archive.org snapshot
Promise
APIPromise
is supported in
all current browsers except IE11
Show archive.org snapshot
jQuery has shipped with the promise-like
$.Deferred()
Show archive.org snapshot
for many years.
.then()
on resolved promises runs callbacks synchronously!async
/await
will always transpile to global.Promise
and you want async
/ await
eventually.I recommend to just use native Promise
and polyfill with the 2 KB or
Zousan
Show archive.org snapshot
. You can even polyfill Promise
with jQuery itself using 339 bytes.
We want to test this code:
whenLoaded = new Promise(function(resolve, reject) { $(resolve) })
function foo() { }
function callFooWhenLoaded() { whenLoaded.then(foo) }
This test fails:
describe('callFooWhenLoaded()', function() {
it('calls foo()', function() {
spyOn(window, 'foo')
callFooWhenLoaded()
expect(foo).toHaveBeenCalled()
})
})
The test fails since we're expecting an effect that hasn't happened yet! A test for async code must itself be async.
This new attempt pushes action and expectation into the same microtask:
describe('callFooWhenLoaded()', function() {
it('calls foo()', function() {
spyOn(window, 'foo')
callFooWhenLoaded().then(function() {
expect(foo).toHaveBeenCalled()
})
})
})
However, this test breaks your test runner! Jasmine thinks your test is over too soon.
Finally, here is the correct test. Notice the done
argument:
describe('callFooWhenLoaded()', function() {
it('calls foo()', function(done) {
spyOn(window, 'foo')
callFooWhenLoaded().then(function() {
expect(foo).toHaveBeenCalled()
done()
})
})
})
Note that this would also work:
describe('callFooWhenLoaded()', function() {
it('calls foo()', function(done) {
spyOn(window, 'foo')
callFooWhenLoaded()
Promise.resolve().then(function() {
expect(foo).toHaveBeenCalled()
done()
})
})
})
Can you imagine the footgun when testing that a function has not been called?