Express.js Promises
Promises are a key feature of modern JavaScript, providing a cleaner and more manageable way to handle asynchronous operations. This guide covers key concepts, examples, and best practices for using promises in Express.js applications.
Key Concepts of Promises
- Promise: An object representing the eventual completion (or failure) of an asynchronous operation and its resulting value.
- then(): Method to specify what to do when a promise is fulfilled.
- catch(): Method to specify what to do when a promise is rejected.
- finally(): Method to specify what to do regardless of the promise's outcome.
Creating and Using Promises
Create promises to handle asynchronous operations such as reading files, making HTTP requests, or querying databases:
Example: Creating a Promise
// promises.js
const express = require('express');
const fs = require('fs');
const app = express();
const port = 3000;
function readFile(filename) {
return new Promise((resolve, reject) => {
fs.readFile(filename, 'utf8', (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
}
app.get('/read-file', (req, res) => {
readFile('example.txt')
.then(data => res.send(data))
.catch(err => res.status(500).send('Error reading file'));
});
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}/`);
});
Chaining Promises
Chain multiple promises to handle sequences of asynchronous operations:
Example: Chaining Promises
// chaining-promises.js
const express = require('express');
const fs = require('fs');
const app = express();
const port = 3000;
function readFile(filename) {
return new Promise((resolve, reject) => {
fs.readFile(filename, 'utf8', (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
}
app.get('/read-files', (req, res) => {
readFile('file1.txt')
.then(data1 => {
return readFile('file2.txt').then(data2 => data1 + data2);
})
.then(combinedData => res.send(combinedData))
.catch(err => res.status(500).send('Error reading files'));
});
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}/`);
});
Using Promise.all
Use Promise.all
to run multiple promises concurrently and wait for all of them to complete:
Example: Using Promise.all
// promise-all.js
const express = require('express');
const fs = require('fs');
const app = express();
const port = 3000;
function readFile(filename) {
return new Promise((resolve, reject) => {
fs.readFile(filename, 'utf8', (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
}
app.get('/read-files', (req, res) => {
Promise.all([readFile('file1.txt'), readFile('file2.txt'), readFile('file3.txt')])
.then(results => res.send(results.join('\n')))
.catch(err => res.status(500).send('Error reading files'));
});
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}/`);
});
Error Handling with Promises
Handle errors in promises using the catch
method:
Example: Error Handling with Promises
// promises-error.js
const express = require('express');
const fs = require('fs');
const app = express();
const port = 3000;
function readFile(filename) {
return new Promise((resolve, reject) => {
fs.readFile(filename, 'utf8', (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
}
app.get('/read-file', (req, res) => {
readFile('example.txt')
.then(data => res.send(data))
.catch(err => res.status(500).send('Error reading file'));
});
// Centralized error handling middleware
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send('Something went wrong!');
});
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}/`);
});
Using Async/Await with Promises
Async/await provides a cleaner syntax for working with promises:
Example: Async/Await with Promises
// async-await.js
const express = require('express');
const fs = require('fs').promises;
const app = express();
const port = 3000;
app.get('/read-file', async (req, res) => {
try {
const data = await fs.readFile('example.txt', 'utf8');
res.send(data);
} catch (err) {
res.status(500).send('Error reading file');
}
});
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}/`);
});
Best Practices for Using Promises
- Prefer Async/Await: Use async/await for cleaner and more readable asynchronous code.
- Handle Errors: Always handle errors in your promises to prevent unhandled promise rejections.
- Chain Promises: Chain promises to handle sequences of asynchronous operations effectively.
- Use Promise.all: Use Promise.all to run multiple promises concurrently and wait for all of them to complete.
- Avoid Callback Hell: Use promises to avoid deeply nested callbacks and improve code readability.
Testing Promises
Test your promise-based code using frameworks like Mocha, Chai, and Supertest:
Example: Testing Promises
// Install Mocha, Chai, and Supertest
// npm install --save-dev mocha chai supertest
// test/promises.test.js
const chai = require('chai');
const expect = chai.expect;
const request = require('supertest');
const express = require('express');
const fs = require('fs').promises;
const app = express();
app.get('/read-file', (req, res) => {
fs.readFile('example.txt', 'utf8')
.then(data => res.send(data))
.catch(err => res.status(500).send('Error reading file'));
});
describe('GET /read-file', () => {
it('should read the file content', (done) => {
request(app)
.get('/read-file')
.expect(200)
.end((err, res) => {
if (err) return done(err);
expect(res.text).to.be.a('string');
done();
});
});
it('should return 500 if there is an error reading the file', (done) => {
// Temporarily rename the file to simulate an error
fs.rename('example.txt', 'example_tmp.txt')
.then(() => {
request(app)
.get('/read-file')
.expect(500)
.end((err, res) => {
if (err) return done(err);
expect(res.text).to.equal('Error reading file');
// Restore the file after the test
fs.rename('example_tmp.txt', 'example.txt').then(() => done());
});
});
});
});
// Define test script in package.json
// "scripts": {
// "test": "mocha"
// }
// Run tests with NPM
// npm run test
Key Points
- Promise: An object representing the eventual completion (or failure) of an asynchronous operation and its resulting value.
- then(): Method to specify what to do when a promise is fulfilled.
- catch(): Method to specify what to do when a promise is rejected.
- finally(): Method to specify what to do regardless of the promise's outcome.
- Follow best practices for using promises, such as preferring async/await, handling errors, chaining promises, using Promise.all, and avoiding callback hell.
Conclusion
Promises are a key feature of modern JavaScript, providing a cleaner and more manageable way to handle asynchronous operations. By understanding and implementing the key concepts, examples, and best practices covered in this guide, you can effectively manage asynchronous operations using promises in your Express.js applications. Happy coding!