Working with Promises in JavaScript

Mateen Kiani

Mateen Kiani

Published on Mon Jun 23 2025·4 min read

working-with-promises-in-javascript

Modern JavaScript applications rely heavily on asynchronous operations—fetching data, reading files, or querying databases. Promises provide a clean, chainable approach to handle these operations without falling into the infamous “callback hell.” In this guide, we'll explore how to create, chain, and manage errors with Promises, plus practical patterns that you can drop into your next project.

Whether you're writing front-end code that calls APIs or back-end scripts in Node.js, mastering Promises will help you write more readable and maintainable code. Let’s dive in!


Creating a Promise

A Promise represents a value that may be available now, later, or never. You can create one using the new Promise constructor, which takes an executor callback:

const myPromise = new Promise((resolve, reject) => {
// Simulate async operation
setTimeout(() => {
const success = Math.random() > 0.3;
if (success) {
resolve('Operation succeeded!');
} else {
reject(new Error('Something went wrong'));
}
}, 1000);
});

After 1 second, the promise either resolves or rejects. Use .then() and .catch() to handle each outcome:

myPromise
.then(result => console.log(result))
.catch(error => console.error(error.message));

Tip: Always reject with an Error object for consistent stack traces.


Chaining Promises

One of the biggest advantages of Promises is chaining. Each .then() returns a new promise, so you can sequence operations:

fetch('/api/data')
.then(response => response.json())
.then(json => {
console.log('Data received:', json);
return fetch(`/api/more?id=${json.id}`);
})
.then(res => res.json())
.then(data => console.log('More data:', data))
.catch(err => console.error('Error in chain:', err));

This flat structure is far easier to read than nested callbacks.

Advice: Return promises inside .then() so the chain waits properly.


Error Handling Strategies

Proper error handling is critical. You can place a single .catch() at the end of a chain to catch all errors:

doSomething()
.then(stepOne)
.then(stepTwo)
.catch(err => console.error('Caught:', err));

If you need to handle certain errors differently, you can insert intermediate catches:

readConfig()
.catch(err => {
console.warn('Config missing, using defaults');
return defaultConfig;
})
.then(config => initializeApp(config))
.catch(err => console.error('Fatal error:', err));

Note: Each catch returns a promise, so the chain continues.


Promise Utility Methods

JavaScript provides several static methods on Promise to coordinate multiple promises:

  • Promise.all(promises): Waits for all to resolve or rejects fast on the first error.
  • Promise.race(promises): Resolves or rejects as soon as one promise settles.
  • Promise.allSettled(promises): Waits for all to settle, regardless of outcome.
  • Promise.any(promises): Resolves when any promise is fulfilled, rejects if all reject.

Example:

const p1 = fetch('/api/a');
const p2 = fetch('/api/b');
Promise.all([p1, p2])
.then(([resA, resB]) => Promise.all([resA.json(), resB.json()]))
.then(([dataA, dataB]) => console.log(dataA, dataB))
.catch(err => console.error('One failed:', err));

Integrating with Node.js

When working in Node.js, Promises simplify file and network I/O. For example, you can wrap callback-based APIs:

const fs = require('fs');
function readFileAsync(path) {
return new Promise((resolve, reject) => {
fs.readFile(path, 'utf8', (err, data) => {
if (err) return reject(err);
resolve(data);
});
});
}
readFileAsync('./data.json')
.then(contents => console.log(contents))
.catch(console.error);

Or use built-in promise-based methods in modern Node versions:

const fsPromises = require('fs').promises;
fsPromises.readFile('./data.json', 'utf8')
.then(console.log)
.catch(console.error);

You can also fetch HTTP endpoints:

// Learn more about making HTTP calls in Node: [HTTP Requests in Node.js](https://milddev.com/nodejs-how-to-make-http-request)

Pro tip: For file checks, see Check if File Exists.


Best Practices

  • Always return inside .then for proper chaining.
  • Handle errors with a final .catch().
  • Use async/await (built on promises) for more synchronous-looking code.
async function loadData() {
try {
const res = await fetch('/api/data');
const data = await res.json();
console.log(data);
} catch (err) {
console.error('Error loading data:', err);
}
}
  • Clean up resources in finally:
myPromise
.then(doWork)
.catch(handleError)
.finally(cleanup);

Conclusion

Promises are the backbone of modern asynchronous JavaScript. They let you write flat, chainable code, handle errors gracefully, and leverage utility methods to coordinate multiple operations. Whether you’re fetching data in the browser or reading files in Node.js, understanding Promises—and by extension async/await—will make your code more readable and robust.

Start refactoring your callbacks into Promises today. Your future self (and your teammates) will thank you!

Ready to dive deeper? Check out working with JSON files: Save JSON to File.


Mateen Kiani
Mateen Kiani
kiani.mateen012@gmail.com
I am a passionate Full stack developer with around 3 years of experience in MERN stack development and 1 year experience in blockchain application development. I have completed several projects in MERN stack, Nextjs and blockchain, including some NFT marketplaces. I have vast experience in Node js, Express, React and Redux.