Skip to content

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.

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.

// `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.

To make an operation asynchronous, we can use mechanisms like setTimeout, which executes a function after a specified delay without blocking the main thread.

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.

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);

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).
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 rejection
performAsyncTask(-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: 20
Promise 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 async function implicitly returns a Promise.

  • The await keyword can only be used inside an async function. It pauses the execution of the async function until the awaited Promise is settled. If the Promise is fulfilled, await returns the fulfillment value. If rejected, it throws the rejection reason.

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.5s
Sum 1: 11
// after another 0.5s
Sum 2: 21
Attempting sum with negative number...
// after another 0.5s
Calculation 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.
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.