Skip to content

Instantly share code, notes, and snippets.

@anthonyalvarez
Created November 12, 2018 16:44
Show Gist options
  • Select an option

  • Save anthonyalvarez/f7ff181bfbfddf8acc097f2d6cddd9bf to your computer and use it in GitHub Desktop.

Select an option

Save anthonyalvarez/f7ff181bfbfddf8acc097f2d6cddd9bf to your computer and use it in GitHub Desktop.
Callbacks

Callbacks

A function parameter passed to a function. Upon the recieving function's process completion, the parameter function is run.

JavaScript is single threaded, meaning that two bits of script cannot run at the same time; they have to run one after another. In browsers, JavaScript shares a thread with a load of other stuff that differs from browser to browser. But typically JavaScript is in the same queue as painting, updating styles, and handling user actions (such as highlighting text and interacting with form controls). Activity in one of these things delays the others.

As a human being, you're multithreaded. You can type with multiple fingers, you can drive and hold a conversation at the same time. The only blocking function we have to deal with is sneezing, where all current activity must be suspended for the duration of the sneeze. That's pretty annoying, especially when you're driving and trying to hold a conversation. You don't want to write code that's sneezy.

You've probably used events and callbacks to get around this. Here are events:

var img1 = document.querySelector('.img-1');

img1.addEventListener('load', function() {
  // woo yey image loaded
});

img1.addEventListener('error', function() {
  // argh everything's broken
});

This isn't sneezy at all. We get the image, add a couple of listeners, then JavaScript can stop executing until one of those listeners is called.

Unfortunately, in the example above, it's possible that the events happened before we started listening for them, so we need to work around that using the "complete" property of images:

var img1 = document.querySelector('.img-1');

function loaded() {
  // woo yey image loaded
}

if (img1.complete) {
  loaded();
}
else {
  img1.addEventListener('load', loaded);
}

img1.addEventListener('error', function() {
  // argh everything's broken
});

This doesn't catch images that error'd before we got a chance to listen for them; unfortunately the DOM doesn't give us a way to do that. Also, this is loading one image, things get even more complex if we want to know when a set of images have loaded. Events aren't always the best way

Events are great for things that can happen multiple times on the same object—keyup, touchstart etc. With those events you don't really care about what happened before you attached the listener. But when it comes to async success/failure, ideally you want something like this:

img1.callThisIfLoadedOrWhenLoaded(function() {
  // loaded
}).orIfFailedCallThis(function() {
  // failed
});

// and…
whenAllTheseHaveLoaded([img1, img2]).callThis(function() {
  // all loaded
}).orIfSomeFailedCallThis(function() {
  // one or more failed
});

This is what promises do, but with better naming. If HTML image elements had a "ready" method that returned a promise, we could do this:

img1.ready().then(function() {
  // loaded
}, function() {
  // failed
});

// and…
Promise.all([img1.ready(), img2.ready()]).then(function() {
  // all loaded
}, function() {
  // one or more failed
});

At their most basic, promises are a bit like event listeners except: A promise can only succeed or fail once. It cannot succeed or fail twice, neither can it switch from success to failure or vice versa.

If a promise has succeeded or failed and you later add a success/failure callback, the correct callback will be called, even though the event took place earlier.

This is extremely useful for async success/failure, because you're less interested in the exact time something became available, and more interested in reacting to the outcome.

Promise terminology

A promise can be:

  • fulfilled - The action relating to the promise succeeded
  • rejected - The action relating to the promise failed
  • pending - Hasn't fulfilled or rejected yet
  • settled - Has fulfilled or rejected

The spec also uses the term thenable to describe an object that is promise-like, in that it has a then method.

Here's how you create a promise

var promise = new Promise(function(resolve, reject) {
  // do a thing, possibly async, then…

  if (/* everything turned out fine */) {
    resolve("Stuff worked!");
  }
  else {
    reject(Error("It broke"));
  }
});

The promise constructor takes one argument, a callback with two parameters, resolve and reject. Do something within the callback, perhaps async, then call resolve if everything worked, otherwise call reject.

Like throw in plain old JavaScript, it's customary, but not required, to reject with an Error object. The benefit of Error objects is they capture a stack trace, making debugging tools more helpful.

Here's how you use that promise:

promise.then(function(result) {
  console.log(result); // "Stuff worked!"
}, function(err) {
  console.log(err); // Error: "It broke"
});

then() takes two arguments, a callback for a success case, and another for the failure case. Both are optional, so you can add a callback for the success or failure case only.

Chaining

then() isn't the end of the story, you can chain thens together to transform values or run additional async actions one after another.

Transforming values

You can transform values simply by returning the new value:

var promise = new Promise(function(resolve, reject) {
  resolve(1);
});

promise.then(function(val) {
  console.log(val); // 1
  return val + 2;
}).then(function(val) {
  console.log(val); // 3
})

As a practical example, let's go back to:

get('story.json').then(function(response) {
  console.log("Success!", response);
})

The response is JSON, but we're currently receiving it as plain text. We could alter our get function to use the JSON responseType, but we could also solve it in promises land:

get('story.json').then(function(response) {
  return JSON.parse(response);
}).then(function(response) {
  console.log("Yey JSON!", response);
})

Since JSON.parse() takes a single argument and returns a transformed value, we can make a shortcut:

get('story.json').then(JSON.parse).then(function(response) {
  console.log("Yey JSON!", response);
})

In fact, we could make a getJSON() function really easily:

function getJSON(url) {
  return get(url).then(JSON.parse);
}

getJSON() still returns a promise, one that fetches a url then parses the response as JSON.

Queuing asynchronous actions

You can also chain thens to run async actions in sequence. When you return something from a then() callback, it's a bit magic. If you return a value, the next then() is called with that value. However, if you return something promise-like, the next then() waits on it, and is only called when that promise settles (succeeds/fails). For example:

getJSON('story.json').then(function(story) {
  return getJSON(story.chapterUrls[0]);
}).then(function(chapter1) {
  console.log("Got chapter 1!", chapter1);
})

Here we make an async request to story.json, which gives us a set of URLs to request, then we request the first of those. This is when promises really start to stand out from simple callback patterns.

You could even make a shortcut method to get chapters:

var storyPromise;

function getChapter(i) {
  storyPromise = storyPromise || getJSON('story.json');

  return storyPromise.then(function(story) {
    return getJSON(story.chapterUrls[i]);
  })
}

// and using it is simple:
getChapter(0).then(function(chapter) {
  console.log(chapter);
  return getChapter(1);
}).then(function(chapter) {
  console.log(chapter);
})

We don't download story.json until getChapter is called, but the next time(s) getChapter is called we reuse the story promise, so story.json is only fetched once. Yay Promises!

Error handling

As we saw earlier, then() takes two arguments, one for success, one for failure (or fulfill and reject, in promises-speak):

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