Skip to content

Instantly share code, notes, and snippets.

@IanDavey
Last active September 15, 2020 16:59
Show Gist options
  • Save IanDavey/f1cbe26a6f0cd563db8f834362150f77 to your computer and use it in GitHub Desktop.
Save IanDavey/f1cbe26a6f0cd563db8f834362150f77 to your computer and use it in GitHub Desktop.
How to make and keep promises

Promises

To make a brand-new promise to run code in parallel, simply use the Promise constructor:

let myPromise = new Promise((resolve, reject) => {
  // do things in a separate thread
  if (thereWasAnError) {
    reject(errorInfo);
  } else {
    resolve(results);
  }
});
// do things in the main thread

When the promise has completed (ie., resolve() has been called), you can use the results this way:

myPromise.then(results => {
  // do thing with results
});

When the promise has an issue (ie., reject() was called or an unhandled exception occured), you can use the error info this way:

myPromise.catch(errorInfo => {
  // do thing with error
});

If you need to call more asynchronous code within a .then, just return the resulting promise and handle the results in another then:

myPromise.then(results => {
  let anotherPromise = doAsyncThingWith(results);
  return anotherPromise;
}).then(anotherResults => {
  // do more things
});

At the end of it all, you can clean things up with finally

myPromise
.then(results => { })
.catch(err => { })
.finally(() => { });

You can also run multiple promises in parallel:

const totalPromise = Promise.all([promise1, promise2, promise3]);
totalPromise.then(([result1, result2, result3]) => { });

Using async/await

If your JS environment supports it, you can use async and await to handle promises:

let myPromise = doSomethingAsync();
try {
  let results = await myPromise;
  doMoreThings(results);
} catch (err) {
  doErrorThings(err);
} finally {
  cleanupThings();
}

This is more or less equivalent to the following:

let myPromise = doSomethingAsync();
myPromise.then(results => {
  doMoreThings(results);
}).catch(err => {
  doErrorThings(err);
}).finally(() => {
  cleanupThings();
});

When using await, it must be in an async function (the reason why below), which can be declared number of ways:

async function myFunction1(x, y) { }
const myFunction2 = async function(x, y) { };
const myFunction3 = async (x, y) => { };

async is just syntactic sugar that forces a function to return a promise, so the following

async function f(x) {
  let results = await doThingsAsync(x);
  return results;
}

is equivalent to

function f(x) {
  let promise = doThingsAsync(x);
  return promise;
}

NOTE: it turns out this might not actually be the case. Recently we've found uses of await that block the EventLoop (and thus the entire application) that when replaced by simply returning the promise, run just fine.

Thus, when we call an async function, we need to either await the results (requiring the caller to be async as well)

const fResultPromise = f(123);
const fResult = await fResultPromise;

or put a then on the end:

const fResultPromise = f(123);
fResultPromise.then(fResult => { });

Remember, it's just syntactic sugar.

Dealing with "classic" NodeJS callbacks

Sometimes NodeJS libraries do this:

let thing = doThingsAsync(args);
thing.on("stuff", data => { });
thing.on("done", result => { });
thing.on("error", err => { });

We can deal with this simply by wrapping it in a promise and resolving/rejecting in the appropriate callbacks:

let thingPromise = new Promise((resolve, reject) => {
  let thing = doThingsAsync(args);
  thing.on("stuff", data => { });
  thing.on("done", result => resolve(result));
  thing.on("error", err => reject(err));
});
thingPromise.then(result => { }).catch(err => { });

Wrapping it all up in a promise should make it much easier to reason about (and compose with other promises) later.

You can do this with Mongo-like callbacks too:

new Promise((resolve, reject) => {
  MongoClient.connect(url, (err, db) => {
    if (err) reject(err);
    resolve(db);
  });
}).then(db => new Promise((resolve, reject) => {
  db.collection("stuff").find({ _id: 123 }).toArray((err, result) => {
    if (err) reject(err);
    resolve(db, result);
  });
})).then((db, result) => new Promise((resolve, reject) => {
  db.collection("stuff").find({ _id: result[0].foreignID }).toArray((err, result) => {
    if (err) reject(err);
    resolve(db, result);
  });
})).then((db, result) => {
  db.close();
  writeToHttpResponse(result);
});

Or, you can use await in an async function:

let db = await new Promise((resolve, reject) => {
  MongoClient.connect(url, (err, db) => {
    if (err) reject(err);
    resolve(db);
  });
});
let result1 = await new Promise((resolve, reject) => {
  db.collection("stuff").find({ _id: 123 }).toArray((err, result) => {
    if (err) reject(err);
    resolve(result);
  });
});
let result2 = await new Promise((resolve, reject) => {
  db.collection("stuff").find({ _id: result1[0].foreignID }).toArray((err, result) => {
    if (err) reject(err);
    resolve(result);
  });
})
db.close();
writeToHttpResponse(result2);

...though you could probably find a wrapper around MongoDB that does all the promise-wrapping for you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment