Simplifying Asynchronous JavaScript with Promises, Async/Await

supraja
5 min readFeb 6, 2024

--

image credit: link

In JavaScript, handling asynchronous operations can be challenging, especially when dealing with nested callbacks, commonly known as callback hell. This often leads to code that is hard to read and maintain, with error handling duplicated throughout. To address this issue, promises were introduced, providing a cleaner and more organized approach to asynchronous programming.

Asynchronous Calls in JavaScript:

In JavaScript, asynchronous calls are used to execute operations without blocking the main thread, allowing the program to continue with other tasks while waiting for the asynchronous operation to complete. Common examples include API calls, file reading, or setTimeout.

Promises in JavaScript:

Promises are a native implementation introduced in ES6 to handle asynchronous operations. They represent an asynchronous task that will be completed in the future and can be in one of three states: Pending, Fulfilled, or Rejected. Promises use the Promise constructor, which takes a function(executor) with resolve and reject parameters to handle success and failure.

const myPromise = new Promise((resolve, reject) => {
// Asynchronous operation
if (operationSuccessful) {
resolve(result);
} else {
reject(error);
}
});

Before ES6, there was no way to wait for something to perform some operation. For example, when we wanted to make an API call, there was no way to wait until the results came back.

For that, we used to use external libraries like JQuery or Ajax which had their own implementation of promises. But there was no JavaScript implementation of promises.

But then promises were added in ES6 as a native implementation. And now, using promises in ES6, we can make an API call ourselves and wait until it’s done to perform some operation.

Promise Chaining:

Promise chaining is a technique that allows the sequential execution of asynchronous tasks, improving code readability and avoiding callback hell. .then is used to handle the resolved value, and .catch to handle any errors. Chained promises enable a more linear and structured code flow.

myPromise
.then(result => {
// Handle the result
return anotherAsyncOperation(result);
})
.then(finalResult => {
// Handle the final result
})
.catch(error => {
// Handle errors in any of the above promises
});

Delaying Promise Execution:

Many times, we don’t want the promise to execute immediately. Instead, we want it to wait until after some operation is completed. To achieve this, we can wrap the promise in a function and return that promise from that function. This way, we can use the function parameters inside the promise, making the function truly dynamic.

function createPromise(a, b) {
return new Promise(function(resolve, reject) {
setTimeout(function() {
const sum = a + b;
if(isNaN(sum)) {
reject('Error while calculating sum.');
} else {
resolve(sum);
}
}, 2000);
});
}

createPromise(1,8)
.then(function(output) {
console.log(output); // 9
});

// OR

createPromise(10,24)
.then(function(output) {
console.log(output); // 34
});

Note:

  • When creating a promise, it will be either resolved or rejected but not both simultaneously. Therefore, we cannot add two resolve or reject function calls in the same promise.
  • Only a single value can be passed to the resolve or reject function. To pass multiple values to a resolve function, pass them as an object.
const promise = new Promise(function(resolve, reject) {
setTimeout(function() {
const sum = 4 + 5;
resolve({
a: 4,
b: 5,
sum
});
}, 2000);
});

promise.then(function(result) {
console.log(result);
}).catch(function(error) {
console.log(error);
});
// output: {a: 4, b: 5, sum: 9}

Async/Await:

Async/await is a syntactic sugar introduced in ES2017 (ES8) that simplifies asynchronous code and makes it look more synchronous. It allows you to write asynchronous code in a more linear fashion. The async keyword is used to define an asynchronous function, and await is used to pause execution until the promise is resolved.

async function fetchData() {
try {
const result = await myPromise;
console.log(result);
} catch (error) {
console.error(error);
}
}

Benefits of Promises:

  • Error Handling Simplification: Promises provide a dedicated .catch method to handle errors globally, reducing code duplication and improving error handling.
  • Code Readability: Promise chaining allows for a more linear and structured code flow, making the code easier to read and understand.
  • Sequential Execution: Promises enable sequential execution of asynchronous tasks, enhancing code organization and maintainability.

Example:

// Fetch user data from an API
function fetchUserData() {
return new Promise((resolve, reject) => {
// Simulating API request
setTimeout(() => {
const userData = { id: 1, name: 'John', age: 30 };
// Simulate potential error
if (userData) {
resolve(userData);
} else {
reject('Failed to fetch user data');
}
}, 1000);
});
}

// Process user data
function processUserData(userData) {
return new Promise((resolve, reject) => {
// Simulating data processing
setTimeout(() => {
userData.isAdmin = userData.id === 1; // Example processing logic
resolve(userData);
}, 500);
});
}

// Update UI with user data
function updateUI(userData) {
console.log('User Data:', userData);
}

// Fetch user data, process it, and update UI
fetchUserData()
.then(processUserData) // Promise chaining for SEQUENTIAL EXECUTION
.then(updateUI)
.catch(error => {
console.error('Error:', error); // Global ERROR HANDLING
});
// Each .then block clearly represents the next step
// in the asynchronous process, enhancing READABILITY

Promise Methods:

  • Promise.all(iterable): Resolves when all promises in the iterable are resolved, or rejects if any of them are rejected.
const promise1 = Promise.resolve('One');
const promise2 = Promise.resolve('Two');
const promise3 = Promise.resolve('Three');

Promise.all([promise1, promise2, promise3])
.then(values => {
console.log(values); // ['One', 'Two', 'Three']
})
.catch(error => {
console.error(error);
});
  • Promise.race(iterable): Resolves or rejects as soon as one of the promises in the iterable resolves or rejects.
const promise1 = new Promise(resolve => setTimeout(resolve, 1000, 'One'));
const promise2 = new Promise(resolve => setTimeout(resolve, 500, 'Two'));

Promise.race([promise1, promise2])
.then(winner => {
console.log(winner); // 'Two' (whichever resolves first)
})
.catch(error => {
console.error(error);
});
  • Promise.resolve(value): Returns a resolved promise with the given value.
const resolvedPromise = Promise.resolve('Resolved Value');

resolvedPromise
.then(value => {
console.log(value); // 'Resolved Value'
})
.catch(error => {
console.error(error);
});
  • Promise.reject(reason): Returns a rejected promise with the given reason.
const rejectedPromise = Promise.reject('Error Reason');

rejectedPromise
.then(value => {
console.log(value);
})
.catch(reason => {
console.error(reason); // 'Error Reason'
});

These methods provide additional flexibility and control when working with multiple asynchronous tasks.

--

--