Intro
I used to work as Java developer and I remember for the first time when I got in touch with promises in JavaScript. Even though the concept seemed simple, I still couldn’t fully grasp how Promises worked. It changed when I started to use them in projects and understood the cases they solved. Then came the AHA moment and all became more clear. Over time, Promises became a valuable weapon on my toolbelt. It is oddly satisfying when I can use them at work and solve the async handling between functions.
You probably first come across Promises when fetching data from an API, which is also the most common example. Recently, I’ve been interviewed, and guess what was the first question “Can you tell me the difference between Promise and Async Await?”. I welcome that because I see it as a good starting point to know better how the applicant understands how the mechanisms work. However, he or she is mostly using other libraries and frameworks. It let me write down the differences and describe good practices for handling async function errors.
What is the Promise
Let’s start with the initial question: “What is the Promise?” Promise is a placeholder for the value that we don’t know yet but we will get it as a result of asynchronous computation/function. If the promise goes well, then we will get the result. If the promise does not go well, then the promise will return an error.
A basic example of a Promise
Defining a Promise
You define Promise by calling its constructor and passing two callback functions: resolve and reject.
const newPromise = new Promise((resolve, reject) => {
resolve('Hello');
// reject('Error');
});
We call resolve
function when we want to succesfully resolve the Promise. reject
is for rejecting the promise in the case when an error occurs during evaluating our logic.
Retrieving the Promise Result
We use the built-in function then
to get the Promise's result. It has two passed callbacks, result
and error
. The result is called when the Promise is successfully resolved by the function resolve
. If the Promise isn’t resolved, the second function error
is called. This function is triggered either by reject
or by another error that’s thrown.
newPromise.then(result => {
console.log(result); // Hello
}, error => {
console.log("There shouldn't be an error");
});
In our example, we will get the result Hello
because we successfully resolved the Promise.
Error handling of promises
When the Promise is rejected then is always invoked its second error
callback.
const newPromise1 = new Promise((resolve, reject) => {
reject('An error occurred in Promise1');
});
newPromise1.then(
(result) => {
console.log(result); // It is not invoked
},
(error) => {
console.log(error); // 'An error occurred in Promise1'
}
);
A more recommended approach for its clarity is to use the built-in catch
method.
const newPromise2 = new Promise((resolve, reject) => {
reject('An error occurred in Promise2');
});
newPromise2
.then((result) => {
console.log(result); // It is not invoked
})
.catch((error) => {
console.log(error); // 'An error occurred in Promise2'
});
catch
method is chained and has provided its own error callback. It gets invoked when the Promise is rejected.
Both versions work well but the chaining is IMO more readable and it is handy when using other built-in methods that we cover further.
Chaining promises
The result of a promise could likely be another promise. In that case, we can chain an arbitrary number of then
functions.
getJSON('categories.json')
.then(categories => {
console.log('Fetched categories:', categories);
return getJSON(categories[0].itemsUrl);
})
.then(items => {
console.log('Fetched items:', items);
return getJSON(items[0].detailsUrl);
})
.then(details => {
console.log('Fetched details:', details);
})
.catch(error => {
console.error('An error has occurred:', error.message);
});
In our example, it serves to narrow down the search results to get details data. Each then
function can also have its error callback. If we care only about catching any error in the chain of calls, then we can leverage catch
function. It will be evaluated if any of the Promises return an error.
Promise all
Sometimes we want to wait for the results of more independent promises and then act on the results. We can use the built-in function Promise.all
if we don’t care about the order of how the Promises got resolved.
Promise.all([
getJSON('categories.json'),
getJSON('technology_items.json'),
getJSON('science_items.json')
])
.then(results => {
const categories = results[0];
const techItems = results[1];
const scienceItems = results[2];
console.log('Fetched categories:', categories);
console.log('Fetched technology items:', techItems);
console.log('Fetched science items:', scienceItems);
// Fetch details of the first item in each category
return Promise.all([
getJSON(techItems[0].detailsUrl),
getJSON(scienceItems[0].detailsUrl)
]);
})
.then(detailsResults => {
const laptopDetails = detailsResults[0];
const physicsDetails = detailsResults[1];
console.log('Fetched laptop details:', laptopDetails);
console.log('Fetched physics details:', physicsDetails);
})
.catch(error => {
console.error('An error has occurred:', error.message);
});
Promise.all
takes an array of Promises and returns an array of results. If one of the Promises is rejected then Promise.all
is rejected as well.
Racing promises
Another built-in functionality is Promise.race
. It’s used when you have multiple asynchronous functions - Promises - and you want to race them.
Promise.race([
getJSON('technology_items.json'),
getJSON('science_items.json')
])
.then(result => {
console.log('First resolved data:', result);
})
.catch(error => {
console.error('An error has occurred:', error.message);
});
Execution of the Promises can take different times and Promise.race
evaluates the first resolved or rejected Promise from the array. It is used when we don’t care about the order but we want the result of the fastest asynchronous call.
What is Async Await
As you can see, writing Promises requires a lot of boilerplate code. Luckily, we have the native Async Await feature, which makes using Promises even easier. We mark a function by the word async
and by that, we say that somewhere in the code we will be calling asynchronous function and we should not wait for it. Then the async function is called with the await
word.
Basic example of Async Await
const fetchData = async () => {
try {
// Fetch the categories
const categories = await getJSON('categories.json');
console.log('Fetched categories:', categories);
// Fetch items from the first category (Technology)
const techItems = await getJSON(categories[0].itemsUrl);
console.log('Fetched technology items:', techItems);
// Fetch details of the first item in Technology (Laptops)
const laptopDetails = await getJSON(techItems[0].detailsUrl);
console.log('Fetched laptop details:', laptopDetails);
} catch (error) {
console.error('An error has occurred:', error.message);
}
};
fetchData();
Our fetchData
is marked as async
and it allows us to use await
to handle asynchronous calls inside the function. We call more Promises and they will evaluated one after the other.
We use try...catch
block if we want handle the errors. Rejected error is then caught in the catch
block and we can act on it like logging the error.
What’s different
They are both features of JavaScript handling with asynchronous code. The main difference is in the syntax when Promises use chaining with then
and catch
but async await
syntax is more in synchronous way. It makes it easier to read. Error handling for async await
is more straightforward when it leverages try...catch
block. This is a question that you can easily get at the interview. During the answer, you can get deeper into the description of both and highlight those differences.
Promise features
Of course, you can use all the features with async await
. For example Promise.all
.
const fetchAllData = async () => {
try {
// Use await with Promise.all to fetch multiple JSON files in parallel
const [techItems, scienceItems, laptopDetails] = await Promise.all([
getJSON('technology_items.json'),
getJSON('science_items.json'),
getJSON('laptops_details.json')
]);
console.log('Fetched technology items:', techItems);
console.log('Fetched science items:', scienceItems);
console.log('Fetched laptop details:', laptopDetails);
} catch (error) {
console.error('An error occurred:', error.message);
}
};
Practical use cases
Promises are a fundamental feature in JavaScript for handling asynchronous code. Here are the main ways it is used:
Fetching Data from APIs
As was already shown in the examples above, this is one of the most used use cases for Promises and you work with it daily.
Handling file operations
Reading and writing files asynchronously can be done using promises, especially by Node.js module fs.promises
import * as fs from 'fs/promises';
const writeFileAsync = async (filePath, content, options = {}) => {
try {
await fs.writeFile(filePath, content, options);
console.log(`File successfully written to ${filePath}`);
} catch (error) {
console.error(`Error writing file to ${filePath}:`, error.message);
}
};
const filePath = 'output.txt';
const fileContent = 'Hello, this is some content to write to the file!';
const fileOptions = { encoding: 'utf8', flag: 'w' }; // Optional file write options
writeFileAsync(filePath, fileContent, fileOptions);
Promise based libraries
Axios
is library that you should be familiar with. Axios
handles HTTP requests in client and is vastly used.
Express
is a web framework for Node.js. It makes it easy to build web apps and APIs, and when you use promises with Express
, your code stays clean and easy to manage.
Repository with examples
All the examples can be found at: https://github.com/PrincAm/promise-example
Summary
Promises are a fundamental part of JavaScript, essential for handling asynchronous tasks in web development. Whether fetching data, working with files, or using popular libraries like Axios and Express, you’ll frequently use promises in your code.
In this article, we explored what Promises are, how to define and retrieve their results, and how to handle errors effectively. We also covered key features like chaining, Promise.all
, and Promise.race
. Finally, we introduced async await
syntax, which offers a more straightforward way to work with promises.
Understanding these concepts is crucial for any JavaScript developer, as they are tools you’ll rely on daily.
If you haven’t tried it yet, I recommend writing a simple code snippet to fetch data from an API. You can start with a fun API to experiment with. Plus, all the examples and code snippets are available in this repository for you to explore.
Top comments (0)