new Promise in JavaScript: building a promise by hand

The new Promise constructor builds a promise out of resolve and reject: wrapping a callback API, and the two traps that leave your code silent.
4 min read
Believemy logo

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.

new Promise: wrapping a callbackThe executor passed to new Promise receives resolve and reject, and runs immediately. It calls the callback interface, which hands back an error as its first argument and the value as its second. If the error is not null it goes to reject and comes out of catch; otherwise the value goes to resolve and comes out of then. Whichever call comes first settles the promise, later ones are ignored.Wrapping a callback in a promisenew Promise((resolve, reject) => …)the executor runs immediatelycallback(err, value)err ≠ nullreject(err)→ .catch()err = nullresolve(value)→ .then()the first call wins, later ones are ignoredwith neither call, it waits forever


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.

JAVASCRIPT
const wait = (ms) => new Promise((resolve) => {
  setTimeout(() => resolve("done"), ms);
});

wait(200).then((v) => console.log(v));   // done after 200 ms

The 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.

JAVASCRIPT
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));   // dark

Once 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 resolve followed by a reject raises nothing at all: the later ones are ignored in silence. Putting a return in 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 throw from an asynchronous callback, inside a setTimeout, leaves the promise entirely and nobody catches it.
JAVASCRIPT
// 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));
Good to know

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

Question

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.


Question

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.


Question

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.

Related terms

Discover our javaScript glossary

Every word of JavaScript explained simply: keywords, built-in objects, methods, errors and concepts. Clear definitions and examples that actually run, to learn and to troubleshoot.

Share this article

Want to help us? Share this article on your networks or even better: on your site, in an article or in your newsletter.