Understanding Promise and Async/Await: Evolution, Implementation, and Pitfalls
This article traces the evolution of JavaScript asynchronous handling from callbacks to Promise and Async/Await, explains their internal mechanics with code examples, compares implementations, highlights limitations, and discusses how future frontend engineering may address these challenges.
1. Introduction
Having worked in frontend development, I have experienced the transition from Callback Hell to Promise/Deferred, then to Generators, and finally to the widely accepted Async/Await. Both Promise and Async/Await remain active in code, each with its own supporters and criticisms, prompting deeper reflection.
The goal of this article is not to spark debate or promote a single best practice, but to explore the knowledge and anecdotes behind Promise and Async/Await and uncover the consensus hidden within the controversies.
2. Promise
Promise is a more reasonable and powerful solution for asynchronous programming compared to traditional callback hell.
A Promise is essentially a container that stores the result of an operation that will complete in the future. It provides a uniform API for handling various asynchronous operations, and its internal state transitions are irreversible.
The code below demonstrates a typical Promise‑based HTTP request:
<code>function httpPromise(): Promise<{ success: boolean; data: any }> {
return new Promise((resolve, reject) => {
try {
setTimeout(() => {
resolve({ success: true, data: {} });
}, 1000);
} catch (error) {
reject(error);
}
});
}
httpPromise()
.then(res => {})
.catch(error => {})
.finally(() => {});
</code>While syntactically clearer, it can be verbose and lacks debugging convenience.
2.1. How Promise Is Implemented
When I first explored the topic "How to implement a Promise", I wrote a minimal custom implementation:
<code>class promise {
constructor(handler) {
this.resolveHandler = null;
this.rejectedHandler = null;
setTimeout(() => {
handler(this.resolveHandler, this.rejectedHandler);
}, 0);
}
then(resolve, reject) {
this.resolveHandler = resolve;
this.rejectedHandler = reject;
return this;
}
}
function getPromise() {
return new promise((resolve, reject) => {
setTimeout(() => {
resolve(20);
}, 1000);
});
}
getPromise().then(res => {
console.log(res);
}, error => {
console.log(error);
});
</code>This simple version works but cannot handle asynchronous registration correctly, as demonstrated by subsequent test code that throws errors.
<code>const promise1 = getPromise();
setTimeout(() => {
promise1.then(data => {
console.log(data);
}).catch(error => {
console.error(error);
});
}, 0);
</code>For a complete reference, the official Promise implementation can be consulted.
3. Async/Await
Async/Await is not a new concept; it was introduced in C# 5.0 (2012) and later appeared in Python, Scala, and finally in ES2016.
Example in C#:
<code>public async Task<int> SumPageSizesAsync(IList<Uri> uris) {
int total = 0;
foreach (var uri in uris) {
statusText.Text = string.Format("Found {0} bytes ...", total);
var data = await new WebClient().DownloadDataTaskAsync(uri);
total += data.Length;
}
statusText.Text = string.Format("Found {0} bytes total", total);
return total;
}
</code>Equivalent usage in JavaScript:
<code>async function httpRequest(value) {
const res = await axios.post({ ...value });
return res;
}
</code>Notably, a popular Chinese library wind.js implements similar functionality.
3.1. How Async/Await Is Implemented
According to Ruan Yifeng, an async function is essentially syntactic sugar for a Generator function.
Generator example:
<code>const fs = require('fs');
const readFile = function (fileName) {
return new Promise(function (resolve, reject) {
fs.readFile(fileName, function (error, data) {
if (error) return reject(error);
resolve(data);
});
});
};
function* gen() {
const f1 = yield readFile('/etc/fstab');
const f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
}
</code>The same logic can be written with async/await:
<code>const asyncReadFile = async function () {
const f1 = await readFile('/etc/fstab');
const f2 = await readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
</code>The transformation essentially replaces the generator’s asterisk (*) with the
asynckeyword and
yieldwith
await.
Improvements over generators include a built‑in executor, clearer semantics, and returning a Promise.
Consequently, an async function always returns a Promise.
Below is a Babel‑generated version of an async function, illustrating how the concise three‑line code expands to dozens of lines:
<code>async function test() {
const img = await fetch('tiger.jpg');
}
// Babel output (simplified)
'use strict';
var test = function () {
var _ref = _asyncToGenerator(regeneratorRuntime.mark(function _callee() {
var img;
return regeneratorRuntime.wrap(function _callee$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
_context.next = 2;
return fetch('tiger.jpg');
case 2:
img = _context.sent;
case 3:
case 'end':
return _context.stop();
}
}
}, _callee, this);
}));
return function test() {
return _ref.apply(this, arguments);
};
}();
function _asyncToGenerator(fn) {
return function () {
var gen = fn.apply(this, arguments);
return new Promise(function (resolve, reject) {
function step(key, arg) {
try {
var info = gen[key](arg);
var value = info.value;
} catch (error) {
reject(error);
return;
}
if (info.done) {
resolve(value);
} else {
return Promise.resolve(value).then(function (value) {
step('next', value);
}, function (err) {
step('throw', err);
});
}
}
return step('next');
});
};
}
</code>While the transformation works, it inflates the source size, which can affect bundle size and load performance, as seen in Vue 3’s decision not to adopt the optional‑chaining operator.
Beyond code size, Async/Await also lacks advanced control flow features such as always, progress, pause, resume, fine‑grained state control, and abort capabilities, relying heavily on try/catch for error handling.
4. Conclusion
Both Promise and Async/Await are excellent solutions for handling frontend asynchronous operations, yet each has shortcomings. As frontend engineering matures, newer approaches will likely emerge to address these limitations, so developers should remain open‑minded and choose the most suitable tool for their needs.
Tencent IMWeb Frontend Team
IMWeb Frontend Community gathering frontend development enthusiasts. Follow us for refined live courses by top experts, cutting‑edge technical posts, and to sharpen your frontend skills.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.