The defining characteristic of node.js, and a primary reason for its success, is its asynchronous "non-blocking" IO (input/output) APIs. In the case of JavaScript, asynchronous means the value will be processed at a later tick of the event loop:
// Note, all of the below ignore errors
console.log(1)
// async/await version
async ()=>{
console.log(await fs.promise.readdir(__dirname))
}()
// Promise version
fs.promise.readdir(__dirname)
.then(files => console.log(files))
// Callback version
fs.readdir(__dirname, (err, files) => console.log(files))
console.log(2)
/* Output:
1
2
['index.js']
['index.js']
['index.js']
*/
Note: The node.js core APIs, by default, use callbacks. These guides will assume you are using songbird
to expose the promise = module.promise.method(...)
helper API (e.g., promise = fs.promise.readdir(...)
).
async/await
Async functions were added to JavaScript in ES7, giving us the ability to use a synchronous-style syntax for asynchronous programming.
Functions marked with the async
keyword, can use await
within their body:
async ()=> {
console.log(await fs.promise.readdir(filename)) // ['index.js']
}
await
pauses the execution of the current stack frame until the asynchronous operation (aka Promise
) resolves.
async
The async
keyword is used to create an asynchronous function.
async ()=> {
// ...
}
In almost every way, async
functions behave exactly like regular functions. They can be passed arguments, return
values, named, nested, invoked, invoked immediately, inherit from Function.prototype
, etc. However, they differ in a few ways.
async
functions:
Promise
that
return
valueawait
within the shallow function body (not in nested functions)async function add(one, two) {
await process.promise.nextTick()
return one + two
}
let promise = add(1, 2)
promise.then(result => console.log(result)) // 3
await
Use await
to wait for a Promise
and its resolution value:
async ()=>{
let filenames = await fs.promise.readdir(__dirname)
console.log(filenames) // ['index.js']
}()
If the promise fails, an error will be thrown instead:
async ()=>{
let filenames = await fs.promise.readdir(' does not exist')
}().catch(e => console.log(e)) // Error: ENOENT: no such file or directory
Further, await
conveniently allows us to use language control-flow features like for
, if
and try/catch
when writing asynchronous code:
async function ls(dirPath) {
let stat = await fs.promise.stat(dirPath)
if (!stat.isDirectory()) return [dirPath]
let filenames = []
for (let name of await fs.promise.readdir(dirPath)) {
let result = await ls(path.join(dirPath, name))
filenames.push(...result)
}
return filenames
}
async ()=> {
try {
console.log(await ls(__dirname))
} catch(e) {
console.log(e.stack)
}
}()
Added to the language in ES6, Promise
API will be the standard idiom (in combination with async/await
) for writing asynchronous JavaScript going forward.
// Consuming a promise API
let promise = fs.promise.readFile(__filename)
promise.then(result => {}, err => {})
// Returning promises
readFile(name) {
return fs.promise.readFile(path.join(__dirname, name))
}
readFile('README.md')
.then(result => {}, err => {})
Note: The Promise
spec makes guarantees that are encapsulated within the API. Unlink callbacks, you need not worry about things like remembering to pass errors, never calling callbacks more than once or whether a control-flow library is compatible with those expectations. There's even a test suite to verify Promise spec compliance.
.then(onFulfilled, onRejected)
newPromise = promise.then(result=>{}, err=>{})
is the primary promise API.
fs.promise.readFile(__filename)
.then(function onFulfilled(result) {
console.log(String(result)),
},
function onRejected(err) {
console.log(err.stack)
})
It's important to note, .then
returns a new promise based on the following logic:
onFulfilled
and onRejected
are optional
promise
is fulfilled, call onFulfilled
with the result
promise
is rejected, call onRejected
with the error
promise
will either be fulfilled or rejected, never both
promise
will call onFulfilled
or onRejected
only once per .then()
promise.then()
returns a new Promise
, newPromise
newPromise
resolves to an error if onFulfilled
or onRejected
throwsnewPromise
resolves to the return value of onFulfilled
or onRejected
promise
is fulfilled, it will skip all onRejected
to the next onFulfilled
promise
is rejected, it will skip all onFulfilled
to the next onRejected
Promise.resolve(1)
.then(res => ++res)
.then(res => ++res)
.then(console.log) // 3
// Skipping onFulfilled/onRejected
Promise.reject(new Error('fail'))
.then(() => console.log('hello')) // Never executes
.then(null, err => err.message) // Convert rejection to fulfillment
.then(null, console.log) // Never executes
.then(console.log) // 'fail'
Recommended: To get more comfortable with Promises, checkout the Nodeschool.io Promise It Won't Hurt Workshopper.
.catch(onRejected)
.catch(onRejected)
is a short-hand for .then(null, onRejected)
Promise.all([promises])
Converts an array of promises to a single promise that is fulfilled when all the promises are fulfilled, and is rejected otherwise.
let promises = [Promise.resolve(1), Promise.resolve(2)]
Promise.all(promises)
.then(console.log) // 1, 2
let promises = [Promise.resolve(1), Promise.reject(new Error('fail')]
Promise.all(promises)
.then(console.log) // Never executes
.catch(console.log) // 'fail'
Noted: Use Promise.all
to wait on promises in parallel. This is especially useful when combined with await
:
async function ls(dirPath) {
let stat = await fs.promise.stat(dirPath)
if (!stat.isDirectory()) return [dirPath]
let promises = []
for (let name of await fs.promise.readdir(dirPath)) {
// Run recursive ls in parallel
let promise = ls(path.join(dirPath, name))
promises.push(promise)
}
// Wait for them in parallel
return _.flatten(await promises)
}
async ()=> {
try {
console.log(await ls(__dirname))
} catch(e) {
console.log(e.stack)
}
}()
The node.js core APIs, by default, use callbacks.
Note: Node.js callbacks are sometimes referred to as "errbacks", "error-backs", "node-backs" or "error-first callbacks" to denote the specific flavor of callbacks in node.js.
Simple Description:
(err, result) => {}
fs.readFile(__filename, (err, result) => {} )
Simple right? But beware, "Here be dragons!"
When writing callbacks, there are numerous unenforced implicit expectations.
Here is the complete Callback Contract:
error instanceof Error === true
Whenever you write a callback, you must remember to follow all of the above requirements or else difficult-to-debug bugs may occur!
You will quickly find this to be both tedious and error-prone. As such, callbacks are by definition anti-patterns and are highly discouraged despite their mass adoption.
When dealing with asynchronous operations, sometimes you must wait on the previous operation's result. Other times, you do not. Sometimes you only care about the end result of a chain of operations. Below are examples of how to handle these scenarios.
Executing 3 tasks in series:
// async/await
async ()=>{
let a = await one()
let b = a + await two()
let c = b + await three()
console.log(c / 2)
}()
// promises
one()
.then(a => two().then(res => res + a))
.then(b => three().then(res => res + b))
.then(c => console.log(c / 2))
// callbacks + async package
async.waterfall([
(callback) => one(callback),
(a, callback) => two((err, res) => callback(err, res + a)),
(b, callback) => three((err, res) => callback(err, res + b)),
(c, callback) => console.log(c / 2)
])
Executing 3 tasks in parallel:
// async/await
async ()=>{
let [a, b, c] = await Promise.all([
one(),
two(),
three()
])
console.log(a + b + c)
}()
// promises + bluebird (for .spread)
Promise.all([
one(),
two(),
three()
]).spread((a, b, c) => console.log(a + b + c))
// callbacks + async package
async.parallel([
(callback) => one(callback),
(callback) => two(callback),
(callback) => three(callback)
], (err, res) => console.log(res[0] + res[1] + res[2]))
Branch is a term used to describe an independent chain of control-flow that results in a single value / outcome. Branches tend to correlate well with moving logic to separate functions.
// async/await
async ()=>{
// some stuff in series
let promiseX = async()=>{
let a = await one()
let b = await two()
return await three()
}
let promiseY = fs.promise.readdir(__dirname)
// some stuff in parallel
let [x, y] = await Promise.all([promiseX, promiseY])
console.log(x + y)
}()
// promises
// some stuff in series
let promiseX = one()
.then(a => two())
.then(b => three())
let promiseY = fs.promise.readdir(__dirname)
// some stuff in parallel
let [x, y] = await Promise.all([promiseX, promiseY])
console.log(x + y)
// callbacks + async package
// some stuff in parallel
async.parallel([
(callback) => {
// some stuff in series
async.waterfall([
(callback) => one(callback),
(a, callback) => two(callback),
(b, callback) => three(callback)
], callback)
},
(callback) => fs.readdir(callback)
], (err, res) => console.log(res[0] + res[1]))
You may not find yourself in all of these scenarios, but they're here for reference.
// Scenario 1: Named function
async function foo() {
// logic
}
promise = foo()
// Scenario 2: Lambda
promise = async ()=>{
// logic
}()
async fooAsync() {
return await fooPromiseReturning()
}
Using bluebird-nodeify
:
async function fnAsync() {
// some logic
}
nodeify(asyncFn(), callback)
Using songbird
:
async function asyncFn() {
// Callback -> Promise
return await fs.promise.readFile(...)
}
Using bluebird-nodeify
:
nodeify(promise, callback)
Using songbird
:
promise = fs.promise.readFile(...)
You can use async/await
semantics in node.js today without babel
by using yield
and "async" Generator Functions (available in v0.12) in combination with APIs like bluebird.coroutine
or co
and co.wrap(function*(){...})
:
$ # Turn on generator support
$ node --harmony --use-strict file-that-uses-generators.js
// Scenario 1: Reusable function*
let fnAsync = co.wrap(function *() {
// yield on promises like async/await
let files = yield fs.promise.readdir(__dirname)
})
let promise = fnAsync()
// Scenario 2: Lambda
let promise = co(function *() {
// yield on promises like async/await
let files = yield fs.promise.readdir(__dirname)
})
Note: In order of recommendation
promise-it-wont-hurt
- Promise
API workshopperlearn-generators
- Async generators are an ES6 hack for writing ES7 async
functions (will teach async/await
but with function*/yield
instead)async-you
- Guide to using the most popular callback control-flow library, async
asyncawait
- Callback heaven with async/await async/await
in JavaScriptasync/await
: The Hero JavaScript DeservedPromise
mistakes explained