Understanding and Mastering JavaScript Promises: Basics, Advanced Usage, and Best Practices
This article provides a comprehensive overview of JavaScript Promises, covering their definition, states, basic usage, error handling, chaining, common APIs, best practices, advanced scenarios such as preloading and cancellation, as well as manual implementation techniques, to help front‑end developers deepen their asynchronous programming skills.
Promise was created to solve asynchronous processing in programs and is ubiquitous in modern front‑end applications, becoming one of the most important skills for front‑end developers.
It not only eliminates callback‑hell but also offers a more complete and powerful solution for asynchronous tasks.
Promise is also a mandatory interview topic for front‑end developers, and mastering it is essential.
Promise Meaning and Basic Introduction
Promise is a native JavaScript class (constructor function). After instantiating it, the asynchronous task is performed.
It accepts an asynchronous task and executes it immediately; when the task finishes, the state becomes either fulfilled (success) or rejected (failure). The state is immutable once set.
Basic Usage
Since Promise is a class, you create an instance with new:
<code style='padding: 15px 16px 16px; color: rgb(171, 178, 191); display: -webkit-box; font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace; font-size: 12px; text-align: justify'>const myPromise = new Promise((resolve, reject) => {
// Here is the Promise body, executing an async task
ajax('xxx', () => {
resolve('success'); // or reject('failure')
})
});</code>After the instance is created, its body runs immediately (e.g., a request is sent). When the request succeeds, call resolve(data) to mark the state as fulfilled, then handle the result in then():
<code style='padding: 15px 16px 16px; color: rgb(171, 178, 191); display: -webkit-box; font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace; font-size: 12px; text-align: justify'>const myPromise = new Promise((resolve, reject) => {
// Promise body
ajax('xxx', () => {
resolve('success');
})
});
myPromise.then(data => {
// Process data
});</code>Note that the Promise body runs synchronously at initialization; only the callbacks inside then() are asynchronous, which is often examined in interviews.
<code style='padding: 15px 16px 16px; color: rgb(171, 178, 191); display: -webkit-box; font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace; font-size: 12px; text-align: justify'>const myPromise = new Promise((resolve, reject) => {
console.log(1);
ajax('xxx', () => {
resolve('success');
})
}).then(() => {
console.log(2);
});
console.log(3); // Output: 1, 3, 2</code>If the asynchronous task finishes before then() is called, the callback will still be queued as a micro‑task and executed after the current call stack.
<code style='padding: 15px 16px 16px; color: rgb(171, 178, 191); display: -webkit-box; font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace; font-size: 12px; text-align: justify'>const myPromise = new Promise((resolve, reject) => {
console.log(1);
resolve();
}).then(() => {
console.log(2);
});
console.log(3); // Output: 1, 3, 2</code>Promise Error Handling
Method 1: Provide a second argument to then().
<code style='padding: 15px 16px 16px; color: rgb(171, 178, 191); display: -webkit-box; font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace; font-size: 12px; text-align: justify'>const myPromise = new Promise(...);
myPromise.then(successCallback, errorCallback);
</code>Method 2 (recommended): Use catch().
<code style='padding: 15px 16px 16px; color: rgb(171, 178, 191); display: -webkit-box; font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace; font-size: 12px; text-align: justify'>const myPromise = new Promise(...);
myPromise.then(successCallback).catch(errorCallback);
</code>Method 3: Traditional try...catch (only catches synchronous errors).
Chaining
Each call to then() or catch() returns a new Promise, allowing further chaining:
<code style='padding: 15px 16px 16px; color: rgb(171, 178, 191); display: -webkit-box; font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace; font-size: 12px; text-align: justify'>const myPromise = new Promise(resolve => {
resolve(1);
}).then(data => {
return data + 1;
}).then(data => {
console.log(data); // Output: 2
});
</code>If you call then() on the original Promise each time, you always get the original result:
<code style='padding: 15px 16px 16px; color: rgb(171, 178, 191); display: -webkit-box; font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace; font-size: 12px; text-align: justify'>const myPromise = new Promise(resolve => {
resolve(1);
});
myPromise.then(data => {
return data + 1;
});
myPromise.then(data => {
console.log(data); // Output: 1
});
</code>Common APIs
Promise provides instance (prototype) methods and static (class) methods. Instance methods can be chained.
then() : Called when the Promise is fulfilled.
catch() : Called when the Promise is rejected or an error occurs.
finally() : Executes regardless of fulfillment or rejection.
Promise.resolve() : Returns a fulfilled Promise; useful for creating micro‑tasks.
<code style='padding: 15px 16px 16px; color: rgb(171, 178, 191); display: -webkit-box; font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace; font-size: 12px; text-align: justify'>console.log(1);
Promise.resolve().then(() => {
console.log(2); // micro‑task
});
console.log(3);
</code>Promise.reject() : Returns a rejected Promise.
Promise.all() : Waits for all supplied Promises to settle (fulfilled or rejected) and resolves with an array of results.
<code style='padding: 15px 16px 16px; color: rgb(171, 178, 191); display: -webkit-box; font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace; font-size: 12px; text-align: justify'>const p1 = new Promise(...);
const p2 = new Promise(...);
const p3 = new Promise(...);
Promise.all([p1, p2, p3]).then(list => {
// All succeeded
}).catch(() => {
// At least one failed
});
</code>Promise.race() : Resolves or rejects as soon as any supplied Promise settles.
Best Practices for Promise
While Promise APIs are simple, complex modules can become confusing. The following points help you use them effectively.
Two Key Points for Asynchronous Promise‑ification
Encapsulate every asynchronous operation in a Promise, e.g., replace a ready(callback) method with ready().then(). Keep the new Promise creation inside the library rather than in business code.
<code style='padding: 15px 16px 16px; color: rgb(171, 178, 191); display: -webkit-box; font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace; font-size: 12px; text-align: justify'>function getData() {
const promise = new Promise((resolve, reject) => {
ajax(xxx, d => {
resolve(d);
})
});
return promise;
}
getData().then(data => {
console.log(data);
});
</code>The essential steps are defining the async task content and indicating when the task ends (via resolve or reject).
How to Avoid Redundant Wrapping?
Many libraries already return Promises; avoid wrapping them again. For example, the following is redundant:
<code style='padding: 15px 16px 16px; color: rgb(171, 178, 191); display: -webkit-box; font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace; font-size: 12px; text-align: justify'>function getData() {
return new Promise(resolve => {
axios.get(url).then(data => {
resolve(data);
})
})
}
</code>Instead, use Promise.resolve directly:
<code style='padding: 15px 16px 16px; color: rgb(171, 178, 191); display: -webkit-box; font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace; font-size: 12px; text-align: justify'>function getData() {
const a = 1;
const b = 2;
const c = a + b;
return Promise.resolve(c);
}
</code>Error Handling
Prefer catch() to capture both Promise‑internal and then() errors. Once an error is caught, it will not propagate further unless a new error is thrown inside the catch.
<code style='padding: 15px 16px 16px; color: rgb(171, 178, 191); display: -webkit-box; font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace; font-size: 12px; text-align: justify'>const p = new Promise((resolve, reject) => {
reject('error');
}).catch(err => {
console.log('caught');
}).catch(() => {
// Not executed
}).then(() => {
console.log('success'); // Executed
});
</code>Using async/await
Async functions return a Promise and allow more readable code. Remember that await must be followed by a Promise; if you need to wait for a timeout, wrap it in a Promise.
<code style='padding: 15px 16px 16px; color: rgb(171, 178, 191); display: -webkit-box; font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace; font-size: 12px; text-align: justify'>async function getData() {
const data = await axios.get(url);
return data;
}
// Incorrect usage
async function bad() {
const data = await setTimeout(() => {}, 3000); // setTimeout does not return a Promise
}
// Correct usage
async function good() {
const data = await new Promise(resolve => {
setTimeout(() => {
resolve();
}, 3000);
});
console.log('3 seconds passed');
}
</code>Advanced Promise Applications
Preloading Application
When a page has large data, you can start a network request and a cache‑initialization request simultaneously. Whichever resolves first provides the data, reducing perceived load time.
Cancellation Scenario
If a component is unmounted while requests are in flight, you should cancel the pending Promises to avoid errors.
<code style='padding: 15px 16px 16px; color: rgb(171, 178, 191); display: -webkit-box; font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace; font-size: 12px; text-align: justify'>useEffect(() => {
let dataPromise = new Promise(...);
let data = await dataPromise();
// Process data – component might be destroyed here
return () => {
// Cancel or abort dataPromise here
};
});
</code>One approach is to wrap the original Promise with a cancelable Promise that races against a manually rejected Promise.
<code style='padding: 15px 16px 16px; color: rgb(171, 178, 191); display: -webkit-box; font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace; font-size: 12px; text-align: justify'>function getPromiseWithCancel(originPromise) {
let cancel = () => {};
let isCancel = false;
const cancelPromise = new Promise((resolve, reject) => {
cancel = e => {
isCancel = true;
reject(e);
};
});
const groupPromise = Promise.race([originPromise, cancelPromise])
.catch(e => {
if (isCancel) {
return new Promise(() => {}); // swallow cancellation
} else {
return Promise.reject(e);
}
});
return Object.assign(groupPromise, { cancel });
}
const originPromise = axios.get(url);
const promiseWithCancel = getPromiseWithCancel(originPromise);
promiseWithCancel.then(data => {
console.log('render', data);
});
promiseWithCancel.cancel(); // abort
</code>In‑Depth Understanding of Promise: Inversion of Control
Unlike callbacks where the encapsulation layer invokes the business‑provided function, Promise inverts control: the business layer receives a Promise object, and the encapsulation layer only signals completion via resolve / reject. The business layer then decides when and how to handle the result using then() or catch().
How to Manually Implement a Promise Class
To understand Promise deeply, you can try implementing a minimal version before ES6 introduced it. The class needs a constructor that receives an executor function, three states (pending, fulfilled, rejected), and methods like then, catch, and finally.
<code style='padding: 15px 16px 16px; color: rgb(171, 178, 191); display: -webkit-box; font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace; font-size: 12px; text-align: justify'>class MyPromise {
constructor(fun) {
this.status = 'pending'; // pending, fulfilled, rejected
fun(this.resolve.bind(this), this.reject.bind(this));
}
resolve() {} // implementation omitted
reject() {} // implementation omitted
then() {}
catch() {}
}
</code>Fill in the logic for state transitions, callback storage, and chaining to complete the implementation.
Conclusion
This article introduced the basic Promise API, explored practical usage scenarios, and highlighted interview‑relevant points such as async/await and manual implementation. By mastering these concepts, you can confidently handle asynchronous logic in front‑end development.
Below is a challenging execution‑order question for practice:
<code style='padding: 15px 16px 16px; color: rgb(171, 178, 191); display: -webkit-box; font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace; font-size: 12px; text-align: justify'>function promise2() {
return new Promise(resolve => {
console.log('promise2 start');
resolve();
})
}
function promise3() {
return new Promise(resolve => {
console.log('promise3 start');
resolve();
})
}
function promise4() {
return new Promise(resolve => {
console.log('promise4 start');
resolve();
}).then(() => {
console.log('promise4 end');
})
}
async function asyncFun() {
console.log('async1 start');
await promise2();
console.log('async1 inner');
await promise3();
console.log('async1 end');
}
setTimeout(() => {
console.log('setTimeout start');
promise1();
console.log('setTimeout end');
}, 0);
asyncFun();
promise4();
console.log('script end');
</code>ByteFE
Cutting‑edge tech, article sharing, and practical insights from the ByteDance frontend team.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
