Most of the time nobody writes new Promise: an async function already returns one, and modern interfaces produce them on their own.
One case still calls for it: when the work you are waiting on only announces its end through a callback.
Definition
new Promise takes a function, called the executor, which receives two control functions: resolve to deliver a value, reject to report a failure. The executor starts immediately, the moment the object is created.
const wait = (ms) => new Promise((resolve) => {
setTimeout(() => resolve("done"), ms);
});
wait(200).then((v) => console.log(v)); // done after 200 msThe promise stays pending as long as neither resolve nor reject has been called. A Promise that calls neither does not fail: it simply never settles, and the await in front of it waits forever.
Wrapping a callback interface
This is the constructor's main job. A function following the error-first convention converts in one move: the error goes to reject, the result goes to resolve.
function readConfig(path, callback) {
if (!path) return callback(new Error("Missing path"));
callback(null, { theme: "dark" });
}
const readConfigPromise = (path) => new Promise((resolve, reject) => {
readConfig(path, (err, value) => {
if (err) reject(err);
else resolve(value);
});
});
readConfigPromise("app.json").then((c) => console.log(c.theme)); // darkOnce that wrapper is in place, the call joins the rest of the code: then(), catch, await and Promise.all() all work on it exactly as on any other promise.
The two traps of the constructor
- The first call wins. A
resolvefollowed by arejectraises nothing at all: the later ones are ignored in silence. Putting areturnin front of each call saves you from ever coming back to it. - The executor is only protected on the surface. A throw written directly in the executor does become a rejection. But a
throwfrom an asynchronous callback, inside asetTimeout, leaves the promise entirely and nobody catches it.
// Becomes a rejection: the throw sits in the executor
new Promise(() => { throw new Error("visible"); })
.catch((err) => console.log("caught:", err.message));
// Becomes nothing: the throw leaves later, outside the executor
// new Promise(() => setTimeout(() => { throw new Error("lost"); }, 0));Promise.resolve(value) and Promise.reject(error) build an already-settled promise in a single expression. Handy for a cache: the function hands back either an immediate value or a network call, and the caller never has to tell the two apart.
Frequently asked questions
Can an async function replace the constructor?
Almost always, yes. If the work you are waiting on already returns a promise, wrapping the call in new Promise only adds a layer and one more chance to drop a rejection. The constructor earns its place only against a callback, an event, or an interface that knows nothing about promises.
What happens when a promise is passed to resolve?
It gets adopted: the outer promise takes on the outcome of the inner one instead of delivering a nested object. That is also what makes a return of a promise transparent inside a then, and what makes a promise of a promise impossible to obtain.
Can the executor be marked async?
Technically yes, but it is a known defect. Errors thrown inside an async executor become the rejection of the inner promise, which is wired to nothing, and so vanish from the diagnosis entirely. Keep a plain executor and do the asynchronous work outside of it.