Promises and Callbacks are both ways to handle asynchronous operations in JavaScript, but they have significant differences in design philosophy, usage, and code readability.
Callbacks
Basic Concept
A callback is a function passed as an argument to another function, called after the asynchronous operation completes.
Basic Usage
javascriptfunction fetchData(callback) { setTimeout(() => { const data = { name: 'John', age: 30 }; callback(null, data); }, 1000); } fetchData((error, data) => { if (error) { console.error('Error:', error); return; } console.log('Data:', data); });
Callback Hell Problem
javascript// Callback hell: deeply nested code, hard to maintain fetch('/api/user', (error, userResponse) => { if (error) { console.error(error); return; } userResponse.json((error, user) => { if (error) { console.error(error); return; } fetch(`/api/posts/${user.id}`, (error, postsResponse) => { if (error) { console.error(error); return; } postsResponse.json((error, posts) => { if (error) { console.error(error); return; } console.log('User posts:', posts); }); }); }); });
Promise
Basic Concept
A Promise is an object representing the eventual completion or failure of an asynchronous operation, providing a more elegant way to handle asynchronous operations.
Basic Usage
javascriptfunction fetchData() { return new Promise((resolve, reject) => { setTimeout(() => { const data = { name: 'John', age: 30 }; resolve(data); }, 1000); }); } fetchData() .then(data => console.log('Data:', data)) .catch(error => console.error('Error:', error));
Chaining
javascript// Promise chaining: flat code, easy to read fetch('/api/user') .then(response => response.json()) .then(user => fetch(`/api/posts/${user.id}`)) .then(response => response.json()) .then(posts => console.log('User posts:', posts)) .catch(error => console.error('Error:', error));
Main Differences
1. Code Readability
Callbacks:
javascript// Deeply nested, hard to read doSomething1((result1) => { doSomething2(result1, (result2) => { doSomething3(result2, (result3) => { doSomething4(result3, (result4) => { console.log(result4); }); }); }); });
Promise:
javascript// Flat and clear, easy to read doSomething1() .then(result1 => doSomething2(result1)) .then(result2 => doSomething3(result2)) .then(result3 => doSomething4(result3)) .then(result4 => console.log(result4));
2. Error Handling
Callbacks:
javascript// Error handling scattered, easy to miss function fetchData(callback) { setTimeout(() => { if (Math.random() > 0.5) { callback(new Error('Request failed')); } else { callback(null, { data: 'success' }); } }, 1000); } fetchData((error, data) => { if (error) { console.error('Error:', error); return; } console.log('Data:', data); });
Promise:
javascript// Error handling centralized, easy to manage function fetchData() { return new Promise((resolve, reject) => { setTimeout(() => { if (Math.random() > 0.5) { reject(new Error('Request failed')); } else { resolve({ data: 'success' }); } }, 1000); }); } fetchData() .then(data => console.log('Data:', data)) .catch(error => console.error('Error:', error));
3. State Management
Callbacks:
- No clear state concept
- Callbacks can be called multiple times
- Difficult to track async operation state
Promise:
- Three clear states: pending, fulfilled, rejected
- State change is irreversible
- Can query Promise state at any time
javascriptconst promise = new Promise((resolve) => { setTimeout(() => resolve('Completed'), 1000); }); console.log(promise); // Promise {<pending>} setTimeout(() => { console.log(promise); // Promise {<fulfilled>: 'Completed'} }, 1500);
4. Parallel Processing
Callbacks:
javascript// Difficult to process multiple async operations in parallel function fetchAllData(callback) { let completed = 0; const results = []; const urls = ['/api/user', '/api/posts', '/api/comments']; urls.forEach((url, index) => { fetch(url, (error, data) => { if (error) { callback(error); return; } results[index] = data; completed++; if (completed === urls.length) { callback(null, results); } }); }); }
Promise:
javascript// Easy to process multiple async operations in parallel Promise.all([ fetch('/api/user'), fetch('/api/posts'), fetch('/api/comments') ]) .then(responses => Promise.all(responses.map(r => r.json()))) .then(results => console.log('All data:', results)) .catch(error => console.error('Error:', error));
5. Composition and Reuse
Callbacks:
javascript// Difficult to compose and reuse function fetchUser(id, callback) { setTimeout(() => callback(null, { id, name: 'John' }), 1000); } function fetchPosts(userId, callback) { setTimeout(() => callback(null, [{ id: 1, title: 'Post 1' }]), 1000); } // Composing requires nesting fetchUser(1, (error, user) => { if (error) return; fetchPosts(user.id, (error, posts) => { if (error) return; console.log({ user, posts }); }); });
Promise:
javascript// Easy to compose and reuse function fetchUser(id) { return Promise.resolve({ id, name: 'John' }); } function fetchPosts(userId) { return Promise.resolve([{ id: 1, title: 'Post 1' }]); } // Composing is clear fetchUser(1) .then(user => Promise.all([user, fetchPosts(user.id)])) .then(([user, posts]) => console.log({ user, posts }));
Conversion Relationship
Callback to Promise
javascript// Convert callback to Promise function promisify(fn) { return function(...args) { return new Promise((resolve, reject) => { fn(...args, (error, result) => { if (error) { reject(error); } else { resolve(result); } }); }); }; } // Usage example const readFile = promisify(fs.readFile); readFile('file.txt') .then(data => console.log(data)) .catch(error => console.error(error));
Promise to Callback
javascript// Convert Promise to callback function callbackify(promiseFn) { return function(...args) { const callback = args.pop(); promiseFn(...args) .then(result => callback(null, result)) .catch(error => callback(error)); }; } // Usage example const fetchDataCallback = callbackify(fetchData); fetchDataCallback((error, data) => { if (error) { console.error(error); return; } console.log(data); });
Performance Comparison
Memory Usage
Callbacks:
- Lower memory usage
- No need to create additional Promise objects
Promise:
- Slightly higher memory usage
- Each Promise is an object requiring extra memory
Execution Efficiency
Callbacks:
- Slightly higher execution efficiency
- No additional Promise object creation and microtask scheduling
Promise:
- Slightly lower execution efficiency
- Needs to create Promise objects and schedule microtasks
Actual Impact
javascript// Callbacks console.time('callback'); for (let i = 0; i < 10000; i++) { setTimeout(() => {}, 0); } console.timeEnd('callback'); // Promise console.time('promise'); for (let i = 0; i < 10000; i++) { Promise.resolve(); } console.timeEnd('promise');
Use Cases
Scenarios Suitable for Callbacks
- Node.js core modules: fs, http modules use callbacks
- One-time simple operations: Simple async operations don't need Promise complexity
- Performance-sensitive scenarios: Scenarios requiring extreme performance
javascript// Node.js file reading fs.readFile('file.txt', 'utf8', (error, data) => { if (error) { console.error(error); return; } console.log(data); });
Scenarios Suitable for Promises
- Complex async flows: Multiple async operations need composition
- Need error handling: Need centralized error handling
- Modern JavaScript development: async/await syntax sugar
- Frontend development: fetch API, modern browser APIs
javascript// Using async/await async function fetchData() { try { const user = await fetchUser(); const posts = await fetchPosts(user.id); return { user, posts }; } catch (error) { console.error('Error:', error); throw error; } }
Summary
| Feature | Callback | Promise |
|---|---|---|
| Code readability | Prone to callback hell | Chaining, flat code |
| Error handling | Scattered, easy to miss | Centralized, easy to manage |
| State management | No clear state | Three clear states |
| Parallel processing | Manual management needed | Promise.all and other methods |
| Composition and reuse | Difficult to compose | Easy to compose |
| Performance | Slightly higher | Slightly lower |
| Memory usage | Lower | Slightly higher |
| Modern support | Traditional approach | Modern standard |
Best Practices
- Prioritize Promises: In modern JavaScript development, prioritize using Promises
- Use async/await: Make async code look like sync code
- Handle errors: Always add error handling
- Avoid nesting: Keep code flat
- Use tools appropriately: Use Promise.all, Promise.race and other methods
- Consider performance: In performance-sensitive scenarios, consider callbacks