ES6 - Promises
ES6+ - Asynchronous JavaScript: Callbacks, Promises, and Async/Await
Section titled “ES6+ - Asynchronous JavaScript: Callbacks, Promises, and Async/Await”JavaScript is single-threaded, meaning it executes one operation at a time. Asynchronous programming allows operations (like fetching data from a server, reading a file, or timers) to occur without blocking the main thread, keeping the user interface responsive. ES6 introduced Promises as a more robust way to handle asynchronous operations, and later versions of ECMAScript (ES2017) introduced async/await syntax to make working with Promises even more intuitive.
Understanding Callbacks
Section titled “Understanding Callbacks”Before Promises, callbacks were the primary way to manage asynchronous tasks. A callback is a function passed as an argument to another function, which is then invoked inside the outer function to complete some kind of routine or action.
Synchronous Callback Example:
Section titled “Synchronous Callback Example:”// `notifyAll` is a higher-order function that takes two callback functions.function notifyAll(smsNotifier, emailNotifier) { console.log('Starting notification process...'); smsNotifier(); // Execute the first callback emailNotifier(); // Execute the second callback}
notifyAll( function() { // smsNotifier callback console.log("SMS sent."); }, function() { // emailNotifier callback console.log("Email sent."); });
console.log("End of script (synchronous part).");Output for Synchronous Callback:
Starting notification process...SMS sent.Email sent.End of script (synchronous part).In this example, all operations are synchronous. The console.log("End of script...") executes only after notifyAll and its callbacks complete.
Asynchronous Callbacks with setTimeout
Section titled “Asynchronous Callbacks with setTimeout”To make an operation asynchronous, we can use mechanisms like setTimeout, which executes a function after a specified delay without blocking the main thread.
Example:
Section titled “Example:”function notifyAllAsync(smsNotifier, emailNotifier) { // Simulating an asynchronous operation (e.g., network request) setTimeout(function() { console.log('Starting asynchronous notification process...'); smsNotifier(); emailNotifier(); console.log('Asynchronous notification process finished.'); }, 2000); // Executes after 2000 milliseconds (2 seconds)}
notifyAllAsync( function() { console.log("Async SMS sent."); }, function() { console.log("Async Email sent."); });
console.log("End of script (main thread continues). This will log first!");Expected Output (order might vary slightly for the async block depending on console implementation, but ‘End of script’ is first):
End of script (main thread continues). This will log first!// After approximately 2 seconds:Starting asynchronous notification process...Async SMS sent.Async Email sent.Asynchronous notification process finished.The setTimeout schedules the function to run later. The main script continues, logging “End of script…” first. This non-blocking behavior is key to responsive UIs.
The Problem: Callback Hell
Section titled “The Problem: Callback Hell”When dealing with multiple dependent asynchronous operations, relying solely on callbacks can lead to deeply nested code, often called “Callback Hell” or the “Pyramid of Doom.” This makes code hard to read, debug, and maintain.
Example of nested callbacks (illustrative):
Section titled “Example of nested callbacks (illustrative):”setTimeout(function() { console.log("Operation 1 complete"); setTimeout(function() { console.log("Operation 2 complete"); setTimeout(function() { console.log("Operation 3 complete"); // ...and so on }, 1000); }, 1000);}, 1000);ES6 Promises: A Cleaner Approach
Section titled “ES6 Promises: A Cleaner Approach”A Promise is an object representing the eventual completion (or failure) of an asynchronous operation and its resulting value. Promises provide a cleaner way to manage asynchronous sequences and error handling.
- Pending: Initial state, neither fulfilled nor rejected.
- Fulfilled (Resolved): The operation completed successfully, and the promise has a resulting value.
- Rejected: The operation failed, and the promise has a reason for the failure (an error).
Creating a Promise:
Section titled “Creating a Promise:”function performAsyncTask(value) { return new Promise((resolve, reject) => { // Simulate an asynchronous task setTimeout(() => { if (value >= 0) { resolve(`Task successful with value: ${value}`); // Fulfill the promise } else { reject(new Error('Task failed: Value cannot be negative.')); // Reject the promise } }, 1000); });}Consuming a Promise with .then() and .catch():
Section titled “Consuming a Promise with .then() and .catch():”performAsyncTask(10) .then(result => { console.log('Success:', result); // Handles successful fulfillment return performAsyncTask(20); // Chaining another promise }) .then(nextResult => { console.log('Next Success:', nextResult); }) .catch(error => { console.error('Error:', error.message); // Handles any rejection in the chain }) .finally(() => { console.log('Promise settled (either fulfilled or rejected).'); // ES2018 feature });
// Example with rejectionperformAsyncTask(-5) .then(result => { console.log('This will not run:', result); }) .catch(error => { console.error('Error from negative value:', error.message); });Output for the first chain (positive values):
// After 1 second:Success: Task successful with value: 10// After another 1 second:Next Success: Task successful with value: 20Promise settled (either fulfilled or rejected).Output for the second chain (negative value):
// After 1 second:Error from negative value: Task failed: Value cannot be negative..then() is used to schedule a callback for when the promise is fulfilled. It can be chained. .catch() is used to handle errors (rejections). .finally() (ES2018) executes a callback when the promise is settled, regardless of outcome.
ES2017 Async/Await: Syntactic Sugar for Promises
Section titled “ES2017 Async/Await: Syntactic Sugar for Promises”async and await make asynchronous code look and behave a bit more like synchronous code, which can improve readability and maintainability.
-
An
asyncfunction implicitly returns a Promise. -
The
awaitkeyword can only be used inside anasyncfunction. It pauses the execution of theasyncfunction until the awaited Promise is settled. If the Promise is fulfilled,awaitreturns the fulfillment value. If rejected, it throws the rejection reason.
Example using async/await:
Section titled “Example using async/await:”function getSumAsync(n1, n2) { return new Promise((resolve, reject) => { setTimeout(() => { if (n1 < 0 || n2 < 0) { reject(new Error("Negative numbers not supported for sum.")); } resolve(n1 + n2); }, 500); });}
async function calculateSum() { console.log('Starting calculations...'); try { const sum1 = await getSumAsync(5, 6); console.log('Sum 1:', sum1); // 11
const sum2 = await getSumAsync(sum1, 10); console.log('Sum 2:', sum2); // 21
console.log('Attempting sum with negative number...'); const sum3 = await getSumAsync(sum2, -5); // This will throw an error console.log('Sum 3 (will not reach here):', sum3);
} catch (error) { console.error('Calculation Error:', error.message); } finally { console.log('Calculations finished.'); }}
calculateSum();console.log("Async function calculateSum() invoked, main thread continues...");Expected Output:
Async function calculateSum() invoked, main thread continues...Starting calculations...// after 0.5sSum 1: 11// after another 0.5sSum 2: 21Attempting sum with negative number...// after another 0.5sCalculation Error: Negative numbers not supported for sum.Calculations finished.async/await significantly simplifies working with Promises, especially for sequential asynchronous operations and error handling using standard try...catch blocks.
Common Promise Utilities: Promise.all() and Promise.race()
Section titled “Common Promise Utilities: Promise.all() and Promise.race()”JavaScript provides helper functions for working with multiple promises:
Promise.all(iterable): Takes an iterable of promises and returns a single Promise that fulfills when all of the promises in the iterable have fulfilled, or rejects as soon as one of the promises rejects. The fulfillment value is an array of the fulfillment values of the input promises (in the same order).Promise.race(iterable): Takes an iterable of promises and returns a single Promise that fulfills or rejects as soon as one of the promises in the iterable fulfills or rejects, with the value or reason from that promise.
Example of Promise.all():
Section titled “Example of Promise.all():”const promise1 = Promise.resolve(3);const promise2 = 42;const promise3 = new Promise((resolve, reject) => { setTimeout(resolve, 100, 'foo');});
Promise.all([promise1, promise2, promise3]) .then(values => { console.log(values); // [3, 42, "foo"] }) .catch(error => { console.error("One of the promises rejected:", error); });Promises, along with async/await, are fundamental to modern asynchronous JavaScript development, enabling more readable, maintainable, and robust code for handling operations that don’t complete immediately.