Bridging the Gap
While Promises were a massive improvement over callbacks, chaining many .then() calls can still lead to complex logic flows. ECMAScript 2017 introduced async and await, which act as syntactic sugar over Promises. They allow developers to write asynchronous code that looks and behaves more like traditional synchronous code, making it easier to read and debug.
The async Keyword
By prefixing a function with the async keyword, you guarantee that the function will always return a Promise.
- If the function returns a non-promise value, that value is wrapped in a resolved Promise.
- If the function throws an error, the returned Promise is rejected with that error.
The await Operator
The await keyword can only be used inside an async function (or at the top level of an ES module). It pauses the execution of the async function until the Promise it is waiting for settles.
Importantly, await does not block the main thread. It only pauses the execution of the specific async function, allowing the Event Loop to continue processing other tasks.
Error Handling with Try/Catch
One of the greatest benefits of async/await is that it allows the use of standard try/catch/finally blocks for error management, consolidating asynchronous and synchronous error handling into a single pattern.
async function uploadLogs(logs) {
try {
const response = await api.post('/logs', logs);
return response.status;
} catch (error) {
console.error("Upload failed:", error.message);
throw new Error("System failure during upload");
} finally {
clearBuffer();
}
}
The “Sequential” Trap
A common mistake when using async/await is accidentally making independent asynchronous operations sequential, which increases the total execution time.
What happens if you use 'await' on a value that is NOT a Promise?
Implementation Detail: Generators and Iterators
Under the hood, async/await is typically implemented using Generators and yield. An async function is essentially a generator function that is automatically stepped forward by a runner (like the co library used to do) whenever a yielded promise resolves. This allows the function to “pause” its state (local variables, execution point) and resume exactly where it left off, all without blocking the system.