The Troubles with JavaScript Arrow Functions

Arrow functions have surprised me with the amount of turmoil they’ve created. At first glance, they seem so easy, like when multiplying the numbers in an array by 2.

const numbers = [1, 2, 3];
const result = numbers.map(n => n * 2);
// produces [2,4,6]

But even a simple map operation can run into problems if the code tries to map each element to an object literal using the wrong syntax.

const numbers = [1, 2, 3];
const result = numbers.map(n => { value: n });
// produces [undefined], [undefined], [undefined]

The problem in the above code is that the opening curly brace of the arrow function makes JavaScript think there is a block of code to execute instead of a simple object literal expression to evaluate. The result is an array of undefined. In this scenario the code either needs an explicit return statement, or to parenthesize the object literal.

const result = numbers.map(n => ({ value: n }));
// [{value: 1}, {value:2}, {value:3}]

Ironically, most problems developers encounter with arrow functions center around the problem arrow functions attempt to solve. The slippery JavaScript this pointer. A bit of casual reading about arrow functions will tell you how arrow functions capture the this reference from their lexical environment. With arrows, you can write code like the following without worrying about explicitly capturing this into a local variable.

const adder = {
    sum: 0,
    add(numbers) {
        numbers.forEach(n => {
            this.sum += n;
        });
    }
 
};
 
adder.add([1, 2, 3]);
// adder.sum === 6

However, it’s easy to write code that assumes the wrong environment. In the following code we have an arrow function inside an arrow function, so the this reference will not be the adder object, but whatever scope the adder object lives in.

const adder = {
    sum: 0,
    add: (numbers) => { // scope here is important
        numbers.forEach(n => {
            this.sum += n;
        });
    }
 
};
 
adder.add([1, 2, 3]);
// adder.sum === 0

The biggest sea dragon on the map in the arrow function waters is highlighted in the “NOTE” section of 14.2.16 of the spec. The takeaway here is that we cannot change the this reference inside of an arrow function. The reference is fixed, it’s baked, it’s  static and permanent. There are implications for two types of code. First is the type of code that expects to manipulate this using bind, call, or apply.

const adder = {
    sum: 0
};
 
const add = (numbers) => numbers.forEach(n => this.sum += 1);
 
adder.add = add.bind(adder);
 
adder.add([1, 2, 3]);
// adder.sum === 0

The second type of code is code that expects someone else to setup this for a call. I first experienced the brain teaser of unexpected this values writing arrow functions with Jasmine. Jasmine sets this to a context object for sharing state between test setups and asserts. Arrow functions, Jasmine contexts and regular functions mix into a broken cocktail. The same problem can arise with DOM event handlers.

it("this is not what you might expect", () => {
 
    // .. this?
 
});

Summary

Arrow functions are not a replacement for regular functions in JavaScript. There are situations where arrow functions do not work as expected. I’m not giving up on arrow functions, I still use them when possible. However, I do lament the fact that I still need to fret over every use of this in JavaScript code.

JavaScript Promises and Error Handling

Errors in asynchronous code typically require a messy number of if else checks and a careful inspection of parameter values. Promises allow asynchronous code to apply structured error handling. When using promises, you can pass an error handler to the then method or use a catch method to process errors. Just like exceptions in regular code, an exception or rejection in asynchronous code will jump to the nearest error handler.

As an example, let’s use the following functions which log the execution path into a string variable.

var log = "";
 
function doWork() {
    log += "W";
    return Promise.resolve();
}
 
function doError() {
    log += "E";
    throw new Error("oops!");
}
 
function errorHandler(error) {
    log += "H";
}

We’ll use these functions with the following code.

doWork()
    .then(doWork)
    .then(doError)
    .then(doWork) // this will be skipped
    .then(doWork, errorHandler)
    .then(verify);
     
  function verify() {
    expect(log).toBe("WWEH");
    done();
}

The expectation is that the log variable will contain “WWEH” when the code finishes executing, meaning the flow of calls with reach doWork, then doWork, then doError, then errorHandler. There are two observations to make about this result, one obvious, one subtle.

The first observation is that when the call to doError throws an exception, execution jumps to the next rejection handler (errorHandler) and skips over any potential success handlers. This behavior is obvious once you think of promises as a tool to transform asynchronous code into a procedural flow of method calls. In synchronous code, an exception will jump over statements and up the stack to find a catch handler, and the asynchronous code in this example is no different.

What might not be immediately obvious is that the verify function will execute as a success handler after the error. Just like normal execution can resume in procedural code after a catch statement, normal execution can resume with promises after a handled error. Technically, the verify function executes because the error handler returns a successfully resolved promise. Remember the then method always returns a new promise, and unless the error handler explicitly rejects a new promise, the new promise resolves successfully.

A promise object also provides a catch method to handle errors. The last code sample could be written with a catch statement as follows. ]

doWork()
    .then(doWork)
    .then(doError)
    .then(doWork) 
    .then(doWork)
    .catch(errorHandler)
    .then(verify);

The catch method takes only a rejection handler method. There can be a difference in behavior between the following two code snippets:

.then(doWork, errorHandler)

… and …

.then(doWork)
.catch(errorHandler)

In the first code snippet, if the success handler throws an exception or rejects a promise, execution will not go into the error handler since the promise was already resolved at this level. With catch, you can always see an unhandled error from the previous success handler.

Finally, imagine you have a rejected promise in your code, but there is no error handler attached. You can simulate this scenario with the following line of code.

Promise.reject("error!");

Some native environments and promise polyfills will warn you about unhandled promise rejections by displaying a message in the console of the developer tools. An unhandled promise rejection means your application could be missing out on a critical error!