async/await reached Stage 4 this month, meaning it's officially part of the next ECMAScript version (ES2017). After years of callbacks and promise chains, JavaScript finally gets syntax for asynchronous code that reads like synchronous code.
This isn't hyperbole—async/await is the most significant JavaScript language feature since Promises.
The Problem async/await Solves
Asynchronous JavaScript has always been awkward:
Callbacks (hell):
getData(function(a) {
getMoreData(a, function(b) {
getMoreData(b, function(c) {
getMoreData(c, function(d) {
// finally
});
});
});
});
Promises (better but still chained):
getData()
.then(a => getMoreData(a))
.then(b => getMoreData(b))
.then(c => getMoreData(c))
.then(d => {
// finally
});
async/await (readable):
async function process() {
const a = await getData();
const b = await getMoreData(a);
const c = await getMoreData(b);
const d = await getMoreData(c);
// finally
}
The async/await version reads top-to-bottom like synchronous code, but executes asynchronously. This is transformative for code readability.
How It Works
async functions automatically return Promises. await pauses execution until the Promise resolves:
async function fetchUser(id) {
const response = await fetch(`/api/users/${id}`);
const user = await response.json();
return user;
}
// Usage
fetchUser(1).then(user => console.log(user));
Behind the scenes, this compiles to Promises. But you write sequential code. The complexity is hidden.
Error Handling Becomes Natural
With promises, error handling requires .catch() chains. With async/await, use try/catch:
async function fetchUser(id) {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) throw new Error('Not found');
const user = await response.json();
return user;
} catch (error) {
console.error('Failed to fetch user:', error);
return null;
}
}
This is how every other language handles async errors. JavaScript finally caught up.
Parallel Execution
Sequential await is simple but slow. For parallel operations, use Promise.all:
async function fetchMultipleUsers(ids) {
// Sequential (slow)
const users = [];
for (const id of ids) {
users.push(await fetchUser(id));
}
// Parallel (fast)
const users = await Promise.all(
ids.map(id => fetchUser(id))
);
return users;
}
async/await doesn't eliminate Promises—it builds on them. You still need Promise combinators for complex flows.
Browser Support (or Lack Thereof)
async/await is ES2017, not ES2015. Browser support will take years. Edge and Chrome have experimental support. Firefox and Safari are working on it.
For production use today, transpile with Babel:
{
"presets": ["es2017"],
"plugins": ["transform-async-to-generator"]
}
This compiles async/await to generator functions (which Babel then compiles to ES5). The output is verbose but works everywhere.
The pattern is familiar: write future JavaScript, transpile for current browsers. This will be standard practice for years.
The Generator Relationship
async/await is syntactic sugar over generators and Promises. You can implement similar patterns with generators:
function* fetchUser(id) {
const response = yield fetch(`/api/users/${id}`);
const user = yield response.json();
return user;
}
Libraries like co made this work, but the syntax was awkward. async/await standardizes the pattern with clearer semantics.
Understanding generators helps understand async/await, but you don't need generators to use async/await effectively.
Common Patterns
Sequential operations:
const user = await fetchUser(id);
const posts = await fetchPosts(user.id);
Parallel operations:
const [user, posts] = await Promise.all([
fetchUser(id),
fetchPosts(id)
]);
Conditional async:
let user = await fetchFromCache(id);
if (!user) {
user = await fetchFromAPI(id);
await saveToCache(id, user);
}
Loops:
for (const id of userIds) {
await processUser(id); // sequential
}
These patterns cover most async code. The readability improvement over promises or callbacks is substantial.
What Doesn't Change
async/await doesn't eliminate async complexity:
- Race conditions still exist
- Deadlocks are still possible
- Understanding async execution model still matters
- Promises are still the foundation
async/await makes async code more readable. It doesn't make async programming easier fundamentally—just more approachable syntactically.
The Node.js Impact
Node.js is callback-heavy. async/await changes Node development significantly:
// Callback style
fs.readFile('file.txt', (err, data) => {
if (err) return handleError(err);
processData(data);
});
// async/await style
const data = await readFile('file.txt');
processData(data);
Node's core APIs are callback-based. Promisifying them (via util.promisify, coming later) makes async/await natural.
This shift is already happening in userland. Frameworks and libraries are adopting async/await as the primary async pattern.
When It Arrives
ES2017 spec finalizes mid-2017. Browser implementations lag by months/years. Full native support is 2018-2019 at earliest.
But via Babel, async/await is usable today. The transpiled code works, performance is acceptable, and the developer experience is superior.
Waiting for native support means years of inferior async code. Transpiling isn't free but the cost is worth it.
Looking Forward
async/await is JavaScript's answer to async programming that works. Not perfect—it's syntactic sugar over Promises—but dramatically better than what we had.
This is the last major async pattern JavaScript needs. Callbacks → Promises → async/await is a progression to code that's both readable and practical.
Future JavaScript features will build on async/await (async iteration, for await…of) but the foundation is set.
Write async code that reads like sync code. Finally.
Resources: