Broken Promises?

Dropping errors, losing results...

Oct 6th 201810 days ago

Oct 12th 20184 days ago

Are JavaScript Promises Broken?

credit: lennart-heim-766366-unsplash

In the Before Times

One of the most common myths about Promises is it’s alleged error shortcomings.

Many years ago Promises were actually awful with errors. Lots of work went into fixing it.

And lo, it got fixed, even widely deployed.

People rejoiced.

And sadly, some didn’t notice.

The Now Times

The myth still persists, I see it everywhere: popular articles on medium, on dzone, and many other sources.

I’ll admit, even “official” resources and documentation offer mostly flimsy examples and bad habits. These are often used to “prove” the case against Promises. Some even suggest “cures” which make things so much worse.

Rules to Stay Out of Trouble

  1. Always return from your functions.
  2. Always use new with Error’s.
  3. Always use .catch(), at least once.
  4. Prefer named functions.

#1 Promises need something to hold on

It is critical that you always return from your functions.

Promise callback functions follow a certain pattern in .then(callback) and .catch(callback).

Each returned value gets passed to the next .then()’s callback.

function addTen(number) {
  return number + 10;
}

Promise.resolve(10)  // 10
  .then(addTen)      // 20
  .then(addTen)      // 30
  .then(addTen)      // 40
  .then(console.log) // 40

Bonus of “always returning”: code is much easier to unit test.

#2 Errors work best using new

JavaScript has an interesting behavior around errors (which applys to asynchronous and synchronous code.)

In order to get useful details about the line number and call stack, you must use new (the constructor) to get this to work properly. Throwing strings does not work like in Python or Ruby. Making things perhaps more confusing, JavaScript will seem to handle throw "string", as you will see the string in any catch.

Correct new Error examples:

throw new Error('error message')           // ✅
Promise.reject(new Error('error message')) // ✅

The following are common anti-patterns:

throw Error('error message')           // ❌
throw 'error message'                  // ❌
Promise.reject(Error('error message')) // ❌
Promise.reject('error message')        // ❌

#3 Handle errors where it makes sense

Promises provide a slick way to handle errors, using .catch(). It is basically a special kind of .then() - where any errors from preceding .then()’s get handled. Let’s look at an example.

Promise.resolve(42)
  .then(() => 'hello')
  .catch(() => console.log('will not get hit'))
  .then(() => throw new Error('totes fail'))
  .catch(() => console.log('WILL get hit'))

While .catch() may seem like a DOM event handler (i.e. click, keypress). It’s placement is important, as it can only ‘catch’ errors thrown above it.

Overriding errors is relatively trivial Return a non-error value in your .catch() callback, the Promise chain switches to running the .then() callbacks in sequence. (Effectively .)

For example, after we catch the totes fail error below, we return an arbitrary value 99:

Promise.resolve(42)
  .then(() => 'hello')
  .then(() => throw new Error('totes fail'))
  .catch(() => {
    return 99
  })
  .then(console.log) // expected output: 99

#4 Add clarity with named functions 🦄✨

Compare the readability of the following 2 examples:

Anonymous:

Promise.resolve(10)          // 10
  .then(x => x * 2)          // 20
  .then(x => x / 4)          // 5
  .then(x => x * x)          // 25
  .then(x => x.toFixed(2))   // "25.00"
  .then(x => console.log(x)) // expected output: "25.00"

Named:

Promise.resolve(10) // 10
  .then(double)     // 20
  .then(quarter)    // 5
  .then(square)     // 25
  .then(format)     // "25.00"
  .then(log)        // expected output: "25.00"

const double = x => x * 2
const quarter = x => x / 4
const square = x => x * x
const format = x => x.toFixed(2)
const log = x => console.log(x)