Understanding Promises 🤝
Before the widespread adoption of async/await, Promises served as the primary solution for managing asynchronous tasks in JavaScript. A Promise in JavaScript acts as a representation of the eventual completion or failure of an asynchronous operation. It provides a mechanism for handling asynchronous operations by allowing you to attach callbacks to it, thereby mitigating the issue of nested callbacks, commonly known as "callback hell," especially in complex scenarios.
eg -
function fetchData(url) {
return new Promise((resolve, reject) => {
// Simulate an API call
setTimeout(() => {
const data = "Fake data from " + url;
resolve(data);
}, 1000);
});
}
fetchData("https://api.example.com/data")
.then((data) => console.log(data))
.catch((error) => console.error(error));
CONS:
Callback Hell: Although Promises help mitigate callback hell to some extent, complex chains of
.then()
calls can still become difficult to read and maintain, especially in deeply nested scenarios.Verbosity: Working with Promises often involves writing verbose code due to the chaining(also called thening) of
.then()
and.catch()
methods, which can make the codebase harder to understand, especially for beginners.
Entering async
/await
🕚
Async/await, introduced in ES2017, is a syntactic improvement built upon Promises. It enables you to craft asynchronous code that resembles and functions similarly to synchronous code, representing a substantial enhancement in terms of code readability and simplicity.
async function fetchData(url) {
try {
// Simulate an API call
const response = await new Promise((resolve, reject) => {
setTimeout(() => {
const data = "Fake data from " + url;
resolve(data);
}, 1000);
});
console.log(response);
} catch (error) {
console.error(error);
}
}
fetchData("https://api.example.com/data");
Pros:
Simplicity and Readability: The async/await syntax is clear, direct, and instantly comprehensible. 📖
Error Handling: It facilitates intuitive error handling through the use of try/catch blocks, simplifying the process compared to chaining .catch() methods.
Debugging: Debugging asynchronous code becomes simpler with async/await, as execution pauses at await expressions, mimicking synchronous behavior during debugging sessions.
Cons:
Error Propagation: In async/await, errors need to be meticulously handled, or they might be overlooked, potentially resulting in elusive bugs. 🚫
Possible Blocking: Misuse of await can unintentionally block code execution, particularly when used in loops or in serializing unnecessary asynchronous operations, leading to delays in program flow. ⏳
Understanding the Difference in Execution 🖥️
Promises Execution
Promises are executed immediately upon creation. This means that when you create a new Promise, the executor function passed to the constructor is run straight away. The then-catch-finally pattern is used to handle the results of the Promise once it has been resolved or rejected.
Consider this flow:
Initialization: The Promise is created and the executor function runs immediately.
Pending State: Until the asynchronous operation completes, the Promise remains in a pending state.
Settlement: The Promise is either fulfilled with a value (resolved) or an error (rejected).
Chaining:
.then()
is called if the Promise is resolved,.catch()
if it's rejected, and.finally()
runs in both cases after the resolution or rejection.
Promises enforce a clear separation between the initiating of an asynchronous operation and the handling of its result, which can be both a strength and a complexity depending on the situation.
async
/await
Execution
The async
/await
pattern simplifies the chaining of Promises by making asynchronous code look and behave more like synchronous code. This is especially useful when you need to perform a series of asynchronous operations in a specific order.
Here’s the typical execution flow:
Async Function: An
async
function is declared, which implicitly returns a Promise.Awaiting: Within the
async
function,await
is used to pause the execution until the Promise resolves.Sequential Execution: Each
await
call waits for the previous operation to complete before executing the next line, making it easier to follow the code flow.Error Handling:
try
/catch
blocks within anasync
function can catch both synchronous and asynchronous errors, giving a synchronous feel to error handling.
While async
/await
can make code easier to read by reducing the nesting of .then()
and .catch()
methods, it's important to note that it can also lead to performance issues if not used correctly, as it might introduce unnecessary waiting.
Comparing Promises and async
/await
Feature | Promises | async /await |
Syntax | Thenable chain | Syntactic sugar over Promises, uses async and await keywords |
Error Handling | Uses .catch() for errors | Uses try/catch blocks |
Readability | Good for simple chains, but can get complicated with multiple chains | More readable and looks synchronous |
Debugging | Can be challenging in complex promise chains | Easier, as code can be stepped through like synchronous code |
Return Value | Always returns a Promise | async function returns a Promise |
Execution Flow | Non-blocking, always asynchronous | Pauses at each await for the Promise to resolve, making it easier to follow the flow |
Ideal Use Case | Suitable for single or less complex asynchronous operations | Better for handling multiple asynchronous operations in a sequence |
Complex Asynchronous Patterns | Possible but may lead to "callback hell" if not managed properly | Simplifies handling complex asynchronous code patterns |
Community Preference | Widely used before ES2017, still prevalent for simple cases | Increasingly preferred for its simplicity and cleaner code structure |
Promises kick off the moment you create them, and you handle the results with .then()
or .catch()
.
async
/await
makes you wait at each await
for the promise to finish, which can make your code easier to follow but might slow things down if you’re not careful.
So, What Should You Use? 🤔
Use Promises when you have lots of async things happening that don’t depend on each other. They’re great for managing multiple things at once without creating a mess.
async
/await
is awesome when you need things to happen in a specific order, one after the other. It keeps your code clean and easy to read.