Promises in JavaScript provide a structured way to handle asynchronous operations, making it easier to manage tasks like network requests, file reading, or timers without deeply nested callbacks. They represent a eventual completion or failure of a single operation with explicit success and failure paths.
By offering a consistent interface, promises improve code readability, error handling, and composition compared to older patterns, which is especially valuable in modern web applications that rely heavily on non-blocking operations.
| Promise State | Description | Resulting Methods | Typical Use Cases |
|---|---|---|---|
| Pending | Initial state; operation is ongoing | No fulfillment or rejection handlers invoked yet | Starting an API call, awaiting user input |
| Fulfilled | Operation completed successfully | .then() handler receives the result | Received valid data from server |
| Rejected | Operation failed with a reason | .catch() or .then(_, onRejected) receives the error | Network error, invalid response, exception |
Creating Promises with the Constructor
How new Promise Works
The Promise constructor takes an executor function with resolve and reject parameters, giving you direct control over the final state. This low-level approach is useful when wrapping callback-based APIs but should be used sparingly in application code.
Common Pitfalls During Creation
Forgetting to call resolve or reject, or calling them multiple times, can lead to confusing behavior. Always ensure that your executor handles errors and state transitions consistently to avoid silent bugs.
Chaining and Sequential Execution
Building Chains for Flow Control
Each .then can return a value or another promise, enabling sequential operations where later steps depend on earlier results. This chaining pattern keeps asynchronous code flatter and easier to follow compared to nested callbacks.
Error Propagation in Chains
Errors bubble down the chain until caught by a .catch or a rejection handler, which makes it simple to centralize error handling. Returning a rejected promise inside then allows you to conditionally fail a step without breaking the overall flow.
Async Await as Syntactic Sugar
Writing Linear Asynchronous Code
The async and await keywords let you write promise-based logic that looks synchronous, improving readability while preserving non-blocking behavior. This style is especially helpful in complex functions with multiple asynchronous steps.
Handling Exceptions with Try Catch
Surrounding await expressions in try catch blocks gives you familiar error handling mechanics from synchronous code. Unlike .catch, this approach keeps error handling close to the operation that might fail.
Advanced Patterns and Composition
Promise.all for Parallel Tasks
Promise.all runs multiple promises concurrently and delivers an array of results only when all of them resolve. It is ideal for independent operations where you need every result before proceeding.
Promise.race and Early Resolution
Promise.race settles as soon as any contained promise settles, which is useful for timeouts or fallback strategies. This pattern helps enforce deadlines or pick the fastest available response.
Best Practices and Next Steps
- Always handle rejections with .catch or try catch to avoid silent failures
- Prefer async await for complex sequential logic to improve readability
- Use Promise.all for independent parallel tasks to reduce total wait time
- Avoid creating new promises when existing promise-based APIs already suffice
- Centralize error handling in one place to keep business logic cleaner
FAQ
Reader questions
What happens if I return a promise from then?
Returning a promise from a .then handler links it to the outer promise chain, causing the outer chain to wait for that inner promise to settle before continuing. This technique is commonly used to compose asynchronous steps cleanly.
Can a promise be resolved more than once?
No, a promise can only settle once; subsequent calls to resolve or reject are ignored after the first settlement. This behavior ensures a predictable single outcome for each asynchronous operation.
How do unhandled rejections affect my application?
Unhandled rejections can crash your process in Node or surface as warnings in browsers, making robust error handling essential. Always add a .catch at the end of chains or use async await with try catch to capture failures.
Is there a difference between promise and callback hell?
Promises flatten nested structures, making flow control more maintainable and readable. They also standardize success and failure handling, reducing the likelihood of missed errors compared to raw callbacks.