• Stars
    star
    1,603
  • Rank 29,196 (Top 0.6 %)
  • Language
  • License
    MIT License
  • Created about 9 years ago
  • Updated over 7 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

📙 a brief introduction to using Promises in JavaScript

promise-cookbook

This is a brief introduction to using Promises in JavaScript, primarily aimed at frontend developers.

See here for a Chinese translation.

contents

intro

A Promise is a programming construct that can reduce some of the pains of asynchronous programming. Using Promises can help produce code that is leaner, easier to maintain, and easier to build on.

This lesson will mostly focus on ES6 Promise syntax, but will use Bluebird since it provides excellent error handling in the browser. The CommonJS syntax will need a bundler like browserify or webpack. See jam3-lesson-module-basics for an introduction to CommonJS and browserify.

the problem

To demonstrate, let's take the problem of loading images in the browser. The following shows an implementation using a Node style (error-first) callback:

function loadImage(url, callback) {
  var image = new Image();

  image.onload = function() {
    callback(null, image);
  };

  image.onerror = function() {
    callback(new Error('Could not load image at ' + url));
  };

  image.src = url;
}

Tip: The above is implemented on npm as img.

Loading a single image is relatively easy, and looks like this:

loadImage('one.png', function(err, image) {
  if (err) throw err;
  console.log('Image loaded', image);
});

However, as our application grows in complexity, so too does the code. If we were to take the same approach, but load three images, things get a little unwieldy:

loadImage('one.png', function(err, image1) {
  if (err) throw err;

  loadImage('two.png', function(err, image2) {
    if (err) throw err;

    loadImage('three.png', function(err, image3) {
      if (err) throw err;

      var images = [image1, image2, image3];
      console.log('All images loaded', images);
    });
  });
});

This tends to create a "Christmas Tree" of functions; and leads to code that is difficult to read and maintain. Further, if we wanted the images to load in parallel, it would need a more complex solution.

async

There are numerous abstractions built around the error-first callbacks, sometimes called "errbacks."

One way to solve the problem is with the async module:

var mapAsync = require('async').map;

var urls = [ 'one.png', 'two.png' ];
mapAsync(urls, loadImage, function(err, images) {
  if (err) throw err;
  console.log('All images loaded', images);
});

Similar abstractions exist independently on npm, such as:

This approach is very powerful. It's a great fit for small modules as it does not introduce additional bloat or vendor lock-in, and does not have some of the other pitfalls of promises.

However, in a larger scope, promises can provide a unified and composable structure throughout your application. They will also lay the groundwork for ES7 async/await.

promises

Let's re-implement the above with promises for our control flow. At first this may seem like more overhead, but the benefits will become clear shortly.

new Promise(function(resolve, reject) { ... })

Below is how the image loading function would be implemented with promises. We'll call it loadImageAsync to distinguish it from the earlier example.

var Promise = require('bluebird')

function loadImageAsync(url) {
  return new Promise(function(resolve, reject) {
    var image = new Image();

    image.onload = function() {
      resolve(image);
    };

    image.onerror = function() {
      reject(new Error('Could not load image at ' + url));
    };

    image.src = url;
  });
}

The function returns a new instance of Promise which is resolved to image if the load succeeds, or rejected with a new Error if it fails. In our case, we require('bluebird') for the Promise implementation.

The Promise constructor is typically only needed for edge cases like this, where we are converting a callback-style API into a promise-style API. In many cases it is preferable to use a promisify or denodeify utility which converts Node style (error-first) functions into their Promise counterpart.

For example, the above becomes very concise with our earlier loadImage function:

var Promise = require('bluebird');
var loadImageAsync = Promise.promisify(loadImage);

Or with the img module:

var Promise = require('bluebird');
var loadImage = require('img');
var loadImageAsync = Promise.promisify(loadImage);

If you aren't using Bluebird, you can use es6-denodeify for this.

.then(resolved, rejected)

Each Promise instance has a then() method on its prototype. This allows us to handle the result of the async task.

loadImageAsync('one.png')
  .then(function(image) {
    console.log('Image loaded', image);
  }, function(err) {
    console.error('Error loading image', err);
  });

then takes two functions, either of which can be null or undefined. The resolved callback is called when the promise succeeds, and it is passed the resolved value (in this case image). The rejected callback is called when the promise fails, and it is passed the Error object we created earlier.

.catch(err)

Promises also have a .catch(func) to handle errors, which is the same as .then(null, func) but provides clearer intent.

loadImageAsync('one.png')
  .catch(function(err) {
    console.error('Could not load image', err);
  });

chaining

The .then() method always returns a Promise, which means it can be chained. The above could be re-written like so. If a promise is rejected, the next catch() or then(null, rejected) will be called.

In the following example, if the loadImageAsync method is rejected, the only output to the console will be the error message.

loadImageAsync('one.png')
  .then(function(image) {
    console.log('Image loaded', image);
    return { width: image.width, height: image.height };
  })
  .then(function(size) {
    console.log('Image size:', size);
  })
  .catch(function(err) {
    console.error('Error in promise chain', err);
  });

In general, you should be wary of long promise chains. They can be difficult to maintain and it would be better to split the tasks into smaller, named functions.

resolving values

Your then() and catch() callbacks can return a value to pass it along to the next method in the chain. For example, here we resolve errors to a default image:

loadImageAsync('one.png')
  .catch(function(err) {
    console.warn(err.message);
    return notFoundImage;
  })
  .then(function(image) {
    console.log('Resolved image', image);
  });

The above code will try to load 'one.png', but will fall back to using notFoundImage if the load failed.

The cool thing is, you can return a Promise instance, and it will be resolved before the next .then() is triggered. The value resolved by that promise will also get passed to the next .then().

loadImageAsync('one.png')
  .catch(function(err) {
    console.warn(err.message);
    return loadImageAsync('not-found.png');
  })
  .then(function(image) {
    console.log('Resolved image', image);
  })
  .catch(function(err) {
    console.error('Could not load any images', err);
  });

The above tries to load 'one.png', but if that fails it will then load 'not-found.png'.

Promise.all()

Let's go back to our original task of loading multiple images.

The Promise.all() method accepts an array of values or promises and returns a new Promise that is only resolved once all the promises are resolved. Here we map each URL to a new Promise using loadImageAsync, and then pass those promises to all().

var urls = ['one.png', 'two.png', 'three.png'];
var promises = urls.map(loadImageAsync);

Promise.all(promises)
  .then(function(images) {
    console.log('All images loaded', images);
  })
  .catch(function(err) {
    console.error(err);
  });

Finally, things are starting to look a bit cleaner.

passing the buck

You may still be wondering where promises improve on the async approach. The real benefits come from composing promises across your application.

We can "pass the buck" by making named functions that return promises, and let errors bubble upstream. The above code would look like this:

function loadImages(urls) {
  var promises = urls.map(loadImageAsync);
  return Promise.all(promises);
}

A more complex example might look like this:

function getUserImages(user) {
  return loadUserData(user)
    .then(function(userData) {
      return loadImages(userData.imageUrls);
    });
}

function showUserImages(user) {
  return getUserImages(user)
    .then(renderGallery)
    .catch(renderEmptyGallery);
}

showUserImages('mattdesl')
  .catch(function(err) {
    showError(err);
  });

throw and implicit catch

If you throw inside your promise chain, the error will be impliticly caught by the underlying Promise implementation and treated as a call to reject(err).

In the following example, if the user has not activated their account, the promise will be rejected and the showError method will be called.

loadUser()
  .then(function(user) {
    if (!user.activated) {
      throw new Error('user has not activated their account');
    }
    return showUserGallery(user);
  })
  .catch(function(err) {
    showError(err.message);
  });

This part of the specification is often viewed as a pitfall of promises. It conflates the semantics of error handling by combining syntax errors, programmer error (e.g. invalid parameters), and connection errors into the same logic.

It leads to frustrations during browser development: you might lose debugger capabilities, stack traces, and source map details.

debugging

For many developers, this is enough reason to eschew promises in favour of error-first callbacks and abstractions like async.

common patterns

memoization

We can use .then() on a promise even after the asynchronous task is long complete. For example, instead of always requesting the same 'not-found.png' image, we can cache the result of the first request and just resolve to the same Image object.

var notFound;

function getNotFoundImage() {
  if (notFound) {
    return notFound;
  }
  notFound = loadImageAsync('not-found.png');
  return notFound;
}

This is more useful for server requests, since the browser already has a caching layer in place for image loading.

Promise.resolve / Promise.reject

The Promise class also provides a resolve and reject method. When called, these will return a new promise that resolves or rejects to the (optional) value given to them.

For example:

var thumbnail = Promise.resolve(defaultThumbnail);

//query the DB
if (userLoggedIn) {
  thumbnail = loadUserThumbnail();
}

//add the image to the DOM when it's ready
thumbnail.then(function(image) {
  document.body.appendChild(image);
});

Here loadUserThumbnail returns a Promise that resolves to an image. With Promise.resolve we can treat thumbnail the same even if it doesn't involve a database query.

handling user errors

Functions that return promises should always return promises, so the user does not need to wrap them in a try/catch block.

Instead of throwing errors on invalid user arguments, you should return a promise that rejects with an error. Promise.reject() can be convenient here.

For example, using our earlier loadImageAsync:

function loadImageAsync(url) {
  if (typeof url !== 'string') {
    return Promise.reject(new TypeError('must specify a string'));
  }

  return new Promise(function (resolve, reject) {
    /* async code */
  });
}

Alternatively, you could use throw inside the promise function:

function loadImageAsync(url) {
  return new Promise(function (resolve, reject) {
    if (typeof url !== 'string') {
      throw new TypeError('must specify a string');
    }

    /* async code */
  });
}

See here for details.

Promise in ES2015

Although this guide uses bluebird, it should work in any standard Promise implementation. For example, using Babel.

Some other implementations:

For example, in Node/browserify:

// use native promise if it exists
// otherwise fall back to polyfill
var Promise = global.Promise || require('es6-promise').Promise;

pitfalls

In addition to the the issues mentioned in throw and implicit catch, there are some other problems to keep in mind when choosing promises. Some developers choose not to use promises for these reasons.

promises in small modules

One situation where promises are not yet a good fit is in small, self-contained npm modules.

  • Depending on bluebird or es6-promise is a form of vendor lock-in. It can be a problem for frontend developers, where bundle size is a constraint.
  • Expecting the native Promise (ES2015) constructor is also a problem, since it creates a peer dependency on these polyfills.
  • Mixing different promise implementations across modules may lead to subtle bugs and debugging irks.

Until native Promise support is widespread, it is often easier to use Node-style callbacks and independent async modules for control flow and smaller bundle size.

Consumers can then "promisify" your API with their favourite implementation. For example, using the xhr module in Bluebird might look like this:

var Promise = require('bluebird')
var xhrAsync = Promise.promisify(require('xhr'))

complexity

Promises can introduce a lot of complexity and mental overhead into a codebase (evident by the need for this guide). In real-world projects, developers will often work with promise-based code without fully understanding how promises work.

See Nolan Lawson's "We Have a Problem With Promises" for an example of this.

lock-in

Another frustration is that promises tend to work best once everything in your codebase is using them. In practice, you might find yourself refactoring and "promisifying" a lot of code before you can reap the benefits of promises. It also means that new code must be written with promises in mind — you are now stuck with them!

further reading

For a comprehensive list of Promise resources and small modules to avoid library lock-in, check out Awesome Promises.

License

MIT, see LICENSE.md for details.

More Repositories

1

canvas-sketch

[beta] A framework for making generative artwork in JavaScript and the browser.
JavaScript
5,019
star
2

budo

🎬 a dev server for rapid prototyping
JavaScript
2,174
star
3

lwjgl-basics

🔧 LibGDX/LWJGL tutorials and examples
Java
1,841
star
4

graphics-resources

📝 a list of graphic programming resources
1,748
star
5

color-wander

🎨 Generative artwork in node/browser based on a seeded random
JavaScript
1,615
star
6

module-best-practices

📚 some best practices for JS modules
JavaScript
1,521
star
7

workshop-generative-art

A workshop on creative coding & generative art
JavaScript
1,362
star
8

svg-mesh-3d

🚀 converts a SVG path to a 3D mesh
JavaScript
1,169
star
9

workshop-webgl-glsl

A workshop on WebGL and GLSL
JavaScript
1,032
star
10

webgl-wireframes

Stylized Wireframe Rendering in WebGL
JavaScript
713
star
11

workshop-p5-intro

Intro to Creative Coding workshop with p5.js and Tone.js
711
star
12

canvas-sketch-util

Utilities for sketching in Canvas, WebGL and generative art
JavaScript
661
star
13

threejs-app

Some opinionated structure for a complex/scalable ThreeJS app
JavaScript
444
star
14

bellwoods

JavaScript
395
star
15

webgl-lines

some interactive content for a blog post
JavaScript
385
star
16

eases

a grab-bag of modular easing equations
JavaScript
372
star
17

audiograph.xyz

A visual exploration of Pilotpriest's 2016 album, TRANS.
JavaScript
335
star
18

jsconfeu-generative-visuals

Code for the generative projection mapped animations during JSConf EU 2018 in Berlin.
JavaScript
334
star
19

load-asset

Loads a single or multiple assets and returns a promise.
JavaScript
311
star
20

glsl-fxaa

FXAA implementation for glslify in WebGL
GLSL
310
star
21

dictionary-of-colour-combinations

palettes from A Dictionary of Colour Combinations
Python
290
star
22

shader-reload

An interface for reloading GLSL shaders on the fly.
JavaScript
284
star
23

penplot

[DEPRECATED] see canvas-sketch
JavaScript
262
star
24

mp4-wasm

[proof-of-concept] fast MP4 mux / demux using WASM
C
258
star
25

gifenc

fast GIF encoding
JavaScript
246
star
26

codevember

codevember
JavaScript
242
star
27

impressionist

🎨 generative painting using perlin noise for motion
JavaScript
242
star
28

three-line-2d

lines expanded in a vertex shader
JavaScript
224
star
29

three-orbit-controls

orbit controls for ThreeJS
JavaScript
216
star
30

physical-text

🌂 simulating text in the physical world
JavaScript
216
star
31

mp4-h264

[project suspended] MP4 + H264 encoding for the browser with WASM
C
212
star
32

prot

highly opinionated dev environment [Proof of concept]
JavaScript
201
star
33

template-electron-installation

a template for media art installations using Electron in kiosk mode
JavaScript
199
star
34

workshop-web-audio

Web Audio workshop with Frontend Masters
JavaScript
189
star
35

yyz

JavaScript
187
star
36

parametric-curves

JavaScript
185
star
37

fontpath

Font to vector path tools
JavaScript
183
star
38

ghrepo

:octocat: create a new GitHub repo from your current folder
JavaScript
177
star
39

google-panorama-equirectangular

gets equirectangular images from Google StreetView
JavaScript
172
star
40

image-sdf

generate a signed distance field from an image
JavaScript
171
star
41

glsl-film-grain

natural looking film grain using noise functions
JavaScript
171
star
42

subscapes

generative artwork hosted on Ethereum
JavaScript
169
star
43

pack-spheres

Brute force circle/sphere packing in 2D or 3D
JavaScript
161
star
44

polartone

experimental audio visualizer
JavaScript
154
star
45

dom-css

fast dom CSS styling
JavaScript
153
star
46

tiny-artblocks

Toolkit for small ArtBlocks projects
JavaScript
152
star
47

adaptive-bezier-curve

adaptive and scalable 2D bezier curves
JavaScript
138
star
48

atcq

An implementation of Ant-Tree Color Quantization
JavaScript
136
star
49

workshop-data-artwork

material & notes for a workshop on data artwork & creative coding
JavaScript
125
star
50

kami-demos

🚧 Some demos for the Kami WebGL renderer
JavaScript
122
star
51

rust

experiments
JavaScript
122
star
52

kami

🚧 Rendering ecosystem using Node style packaging
JavaScript
120
star
53

looom-tools

Svelte
115
star
54

esmify

parse and handle import/export for browserify
JavaScript
112
star
55

polyline-normals

gets miter normals for a 2D polyline
JavaScript
112
star
56

three-vignette-background

a simple ThreeJS vignette background
JavaScript
111
star
57

simple-input-events

Unified mouse & touch events for desktop and mobile
JavaScript
105
star
58

tweenr

minimal tweening engine
JavaScript
105
star
59

text-modules

✏️ a list of text/font modules
104
star
60

spectrum

a small tool to visualize the frequencies of an audio file
JavaScript
104
star
61

three-shader-fxaa

optimized FXAA shader for ThreeJS
JavaScript
102
star
62

lerp

bare-bones linear interpolation function
JavaScript
101
star
63

canvas-sketch-cli

A CLI used alongside canvas-sketch
JavaScript
92
star
64

svg-path-contours

gets a discrete list of points from svg
JavaScript
90
star
65

pen-plotter-blog-post

JavaScript
90
star
66

simplify-path

simplify 2D polyline of arrays
JavaScript
83
star
67

garnish

🍸 prettifies ndjson from wzrd and similar tools
JavaScript
81
star
68

get-rgba-palette

gets a palette of prominent colors from an array of pixels
JavaScript
81
star
69

keytime

[EXPERIMENT] keyframe animation tools
JavaScript
79
star
70

three-glslify-example

a simple example of ThreeJS with glslify
GLSL
77
star
71

canvas-text

[experiment] better Canvas2D text rendering
JavaScript
77
star
72

raylight

Experimental WebGL Music Visualizer
JavaScript
76
star
73

verlet-system

2D and 3D verlet integration
JavaScript
75
star
74

word-wrapper

wraps words based on arbitrary 2D glyphs
JavaScript
71
star
75

mp4-wasm-encoder

JavaScript
70
star
76

gl-sprite-text

bitmap font rendering for stackgl
JavaScript
69
star
77

tendril-webtoy-blog-post

A blog post for an interactive Tendril web toy
68
star
78

paper-colors

A small set of pastel and off-white paper colors
JavaScript
68
star
79

threejs-tree-shake

Tree-shakes and optimizes ThreeJS apps
JavaScript
66
star
80

gh-readme-scrape

a CLI to bulk download URLs (images/pdfs/etc) from GitHub readmes
JavaScript
65
star
81

fika

A figma plugin generator
JavaScript
60
star
82

shadertoy-export

render ShaderToy demos to PNG
JavaScript
59
star
83

glsl-random

pseudo-random 2D noise for glslify
C
59
star
84

electron-canvas-to-buffer

in Electron, turns a Canvas into a Buffer
JavaScript
55
star
85

gl-vignette-background

a soft gradient background in WebGL
JavaScript
55
star
86

webpack-three-hmr-test

test of ThreeJS + Webpack + HMR
JavaScript
53
star
87

workshop-generative-color

a workshop on color science for generative art and creative coding
JavaScript
52
star
88

filmic-gl

filmic GLSL shaders in ThreeJS
JavaScript
51
star
89

riso-colors

A list of Risograph printer colors
51
star
90

gsx-pdf-optimize

Optimize PDFs with Ghostscript command
JavaScript
50
star
91

raf-loop

a minimal requestAnimationFrame render loop
JavaScript
49
star
92

gdx-swiper

An example of a "Fruit Ninja" style swipe in LibGDX
Java
47
star
93

gsap-promise

promise wrapper for gsap (TweenLite)
JavaScript
47
star
94

browserify-example

a bare-bones, no-bullshit example of using browserify to dev + build a static demo
JavaScript
45
star
95

extract-svg-path

extracts a string of subpaths from an svg file
JavaScript
45
star
96

figma-plugin-palette

"Image Palette" Plugin in Figma
JavaScript
42
star
97

adaptive-quadratic-curve

adaptive and scalable 2D quadratic curves
JavaScript
42
star
98

three-geometry-data

Get vertex and face data from THREE.Geometry
JavaScript
40
star
99

budo-chrome

an extension of budo dev server that supports live script injection
JavaScript
39
star
100

three-tube-wireframe

Builds a tube-based wireframe geometry in ThreeJS
JavaScript
39
star