Arrow functions (also known as ‘rocket’ functions) are concise and convenient. However, they have subtle differences compared to function declarations and function expressions. So how do you know which one to use, and when?

Function declarations and function statements

We have (at least) three ways of creating functions in JavaScript. The first is the function declaration. This binds a function to a given name. It looks something like this:1

// This creates a function bound to the name 'isVowel'
function isVowel(chr) {
  const c = chr.toLowerCase();
  return ['a', 'e', 'i', 'o', 'u'].includes(c);
}

We write the function keyword, followed by an identifier that names the function. In this case, isVowel. Written this way, the function declaration is a statement, rather than an expression.

The second way to create a function is by using a function expression. For example, we could create a function like this:

// Here we write an anonymous function expression, and
// bind it to a name through variable assignment.
const takeWhile = function(predicate, arr) {
  const idx = arr.findIndex((x) => !predicate(x));
  return idx === -1 ? arr : arr.slice(0, idx);
}

We can break down what’s happening here into two steps. First, we create an anonymous function, using the function keyword with no name. Then, we assign this anonymous function to the variable takeWhile. With this syntax, the function declaration is an expression. This means we can assign it to a variable or combine it with other expressions.

Somewhat confusingly, we can also give our function expression a name. One that’s separate from the variable name:

// We assign our function to a named variable, but the
// function expression itself also has a name.
const takeWhile = function takeyWhiley(predicate, arr) {
  const idx = arr.findIndex((x) => !predicate(x));
  return idx === -1 ? arr : arr.slice(0, idx);
}

When we create a function like this, the name takeyWhiley isn’t bound to the scope. This means that we can’t call our function using the takeyWhiley identifier:

// This doesn't work.
takeyWhiley(isVowel, 'aeiobc'.split(''));
// ReferenceError: takeyWhiley is not defined

This throws an error. If we can’t use that name, then what’s the point of it? Well, takeyWhiley will show up if we throw an error in our code. For example:

const takeWhile = function takeyWhiley(predicate, arr) {
  throw new Error('a bad thing happened');
  const idx = arr.findIndex((x) => !predicate(x));
  return idx === -1 ? arr : arr.slice(0, idx);
}

takeWhile(isVowel, 'aeiobc'.split(''));
// Error: a bad thing happened
//     at takeyWhiley (<anonymous>:12:9)
//     at <anonymous>:16:1

When we throw an error, our takeyWhiley name comes back. The JS runtime uses the name from the function expression instead of the bound variable name.

In most ways, though, a function expression acts like a function declaration. But there are a few subtleties to be aware of.

  1. Function declarations are ‘hoisted’ by the JavaScript runtime. The runtime treats them as if you wrote the function at the top of the scope where you declared it. It does this regardless of where you wrote it in the file. Usually, this results in the code doing what you expect. On rare occasions, though, it can cause surprising behaviour.

  2. Suppose you create an anonymous function expression and assign it to a variable. JS engines are generally pretty good at figuring out that the variable name is the function’s name. Sometimes, though, the JS runtime can lose track of variable names. This can be a problem when looking through stack traces as you’re debugging something. So, if that name needs to appear in a stack trace, it’s best to name the function.

Arrow functions

The third way to create a function is using an arrow function expression. These are sometimes called a ‘rocket’ functions. It might look something like this:

const dropWhile = (predicate, arr) => {
  const idx = arr.findIndex((x) => !predicate(x));
  return idx === -1 ? [] : arr.slice(idx);
}

Note that arrow functions are always anonymous and always expressions. We can’t give them names unless we assign them to a variable.

The main difference between an arrow function and the other two is that it’s more concise. We don’t have that verbose function keyword, just two characters that look like an arrow. And they can be even more concise than the example above. If we compress the function body to one expression, we can drop the curly braces and the return keyword. For example, we might refactor dropWhile() to use a helper function, modLength():

// Our modLength() function doesn't have curly braces
// around the function body. It has a single expression
// as its body, and it returns the value of that
// expression when called.
const modLength = (len, x) => x < 0 ? len : x;

// The values we return aren't limited to primitive
// types. They can be arrays, objects, or even new
// functions.
const dropWhile = (predicate, arr) =>
  arr.slice(
    modLength(
      arr.length,
      arr.findIndex((x) => !predicate(x))
    )
  );

We can also drop the round braces if the arrow function takes a single argument. For example:

const CONSONANTS = 'bcdfghjklmnpqrstvwzyz';

// Our isConsonant() function takes a single parameter, c.
const isConsonant = c => CONSONANTS.includes(c.toLowerCase());

Brevity isn’t the only difference between arrow functions and named functions. There are some other key differences, as the MDN page on arrow functions points out:

  • Arrow functions don’t have their own bindings to this, arguments, or super, and should not be used as methods.
  • Arrow functions cannot be used as constructors. Calling them with new throws a TypeError. They also don’t have access to the new.target keyword.
  • Arrow functions cannot use yield within their body and cannot be created as generator functions.

What does all this mean, though? And how do we work out which one to use in our code?

Arrow functions and this

To work out which function type to use, the first question to ask is: Will this function do anything with the this keyword? When this is involved, arrow functions behave differently from functions. For example:

function funcCite() {
  return `${this.title}’ by ${this.author}`
}

const arrowCite = () => `${this.title}’ by ${this.author}`;

On the surface, funcCite() and arrowCite() appear to have identical functionality. But what happens if we use them with an object?

const book = {
  title: 'The Hound of the Baskervilles',
  author: 'Arthur Conan Doyle',
  funcCite: funcCite,
  arrowCite: arrowCite,
}

console.log(book.funcCite());
// 🪵 ‘The Hound of the Baskervilles’ by Arthur Conan Doyle
console.log(book.arrowCite());
// 🪵 ‘undefined’ by undefined

It won’t be an issue if our function doesn’t use the this keyword. But it’s something to be aware of if we work with classes and objects.

Arrow functions and new

Arrow functions don’t work as constructors, so you can’t use the new keyword with them. You may find this sentence puzzling if you’re used to modern JavaScript. You’re most likely used to seeing the new keyword used with classes. For example:

class Book {
  constructor(title, author) {
    this.title = title;
    this.author = author;
  }

  cite() {
    return `${this.title}’ by ${this.author}`;
  }
}

const myBook = new Book(
  'The Hound of the Baskervilles',
  'Arthur Conan Doyle'
);

console.log(myBook.cite());
// 🪵 ‘The Hound of the Baskervilles’ by Arthur Conan Doyle

In the early days, though, JavaScript didn’t have a class keyword. Instead, we used named functions as constructors. To create a class equivalent to the Book example, we’d do something like the following:

function Book(title, author) {
  this.title = title;
  this.author = author;
}

Book.prototype.cite = function() {
  return `${this.title}’ by ${this.author}`;
}

const myBook = new Book(
  'The Hound of the Baskervilles',
  'Arthur Conan Doyle'
);

console.log(myBook.cite());
// 🪵 ‘The Hound of the Baskervilles’ by Arthur Conan Doyle

In fact, under the hood, these two ways of creating a Book class are essentially identical. The class keyword simply provides a more familiar interface for people used to OO languages.

However, we get an error if we try using new with an arrow function:

// Book is now an arrow function.
const Book = (title, author) => {
  this.title = title;
  this.author = author;
}

// This doesn't work.
const myBook = new Book(
  'The Hound of the Baskervilles',
  'Arthur Conan Doyle'
);
// TypeError: Book is not a constructor

Arrow functions and generators

We can’t use the yield keyword inside arrow functions. This means we have to be careful when working with generator functions. The following code, for example, generates a parsing error:

// This doesn't work.
function* oneToTen() {
  // We create an arrow function inside a
  // generator function.
  const yieldEven = (x) => {
    if (x % 2 === 0) {
       yield x;
    }
  }

  for (let i = 1; i <= 10; i++) yieldEven(x);
}
// Expression expected at file:///<filename>:7:8

JavaScript doesn’t have an equivalent of function* for arrow functions. Hence, we must use the function* syntax when working with generators.

Which function syntax should we use?

Thus far, we’ve looked at things arrow functions can’t do. You’d be forgiven for thinking that named function statements are generally better. That’s not always the case, though.

Unless you’re working with generators, it mostly comes down to this. Are you using the this keyword inside your function? If so, you need to understand how each type of function treats the this keyword.

In most cases, arrow functions tend to be more concise. This makes them perfect for anonymous callbacks. For example, they work great with methods like .map() for arrays or Promise.resolve(). It’s safe to use arrow functions most of the time. But there are some cases where you might want the flexibility of function statements.

For example, suppose we’re working with a diverse set of objects. We want to create a common approach to generating logs. We define a method toLogString() for each object type to display a label and the object’s current state. If we were writing TypeScript, the interface might look like this:

interface Loggable {
  toLogString(): string;
}

We define a function, createLogMethod(). It accepts a label, and returns a method that will log an object’s current state. It might look like so:

function createLogMethod(label) {
  return function toLogString() {
    return `[${label}]: ${JSON.stringify(this)}`;
  }
}

We can use it like so:

class Book {
  constructor(title, author) {
    this.title = title;
    this.author = author;
  }
}

// We can assign the method using the class prototype.
Book.prototype.toLogString = createLogMethod('Book');

// Alternatively, we can create a method property
class Sensor {
  constructor(location, uri) {
    this.location = location;
    this.uri = uri;
  }

  toLogString = createLogMethod('Sensor');
}

We can then make a function, log(), that works with any Loggable object:

/** @typedef {{ toLogString(): string }} Loggable */

/** Log
 * @param {Loggable} obj An object that implements the
 *     Loggable interface
 */
const log = (obj) => console.log(obj.toLoggableString());

Since log() doesn’t use the this keyword at all, it’s fine to use a fat arrow function. We can use it with objects like so:

const myBook = new Book(
  'The Hound of the Baskervilles',
  'Arthur Conan Doyle'
);

const mySensor = new Sensor(
  'Baker St',
  'http://example.com/sensors/baker-st'
);

log(myBook);
// 🪵 [Book]: {"title":"The Hound of the Baskervilles","author":"Arthur Conan Doyle"}
log(mySensor);
// 🪵 [Sensor]: {"location":"Baker St","uri":"http://example.com/sensors/baker-st"}

Function statements and hoisting

There is another scenario where you might prefer function statements over arrow functions. This scenario mainly concerns the readability of our code. Perhaps we want to write our code so that the high-level code comes first. And then we follow with the implementation details later. Ordinarily, we can’t use a function expression or an arrow function before it’s defined. But with function declarations, we have more flexibility. For example:

const EXCLUDED_WORDS = ['it', 'a', 'the', 'to', 'do', 'on', 'my', 'is', 'an' ];

function pigLatinifyText(str) {
  // The return statement below is the main body of our
  // function. It calls other functions that haven't
  // been defined yet. The function definitions below
  // are 'hoisted' and treated as if they had been
  // written just above this comment.
  return splitIntoWords()
    .map(latinifyWord)
    .join(' ');

  // Usually, everything after a return statement is
  // ignored. But the function statements below still
  // work.

  function splitIntoWords() {
    return str.split(' ');
  }

  function latinifyWord(word) {
    if (EXCLUDED_WORDS.includes(word)) return word;
    const [prefix, suffix] = partition(
      isConsonant,
      word.split('')
    );
    return `${suffix.join('')}${prefix.join('')}ay`;
  }

  function partition(p, arr) {
    // This is not the most efficient way to write
    // partition(), but it gets the job done.
    return [takeWhile(p, arr), dropWhile(p, arr)];
  }
}

const encoded = pigLatinifyText(
  'there is nothing more deceptive than an obvious fact'
);
console.log(encoded);
// 🪵 erethay is othingnay oremay eceptiveday anthay an obviousay actfay

Putting it all together

We’ve now examined some use cases where you might prefer one function syntax over another. We can use what we’ve learned to create a flow chart that will help us determine which function syntax to use.

A flowchart for deciding which function declaration type to use.
A flowchart for deciding which function declaration type to use.
  1. We start by asking, “Do we need to yield a value?” (Which implies we're working with generators). If ‘yes,’ then we can't use arrow functions.
  2. Next, we ask, “Do we need to work with the this keyword?” If so, we probably want to use method syntax inside a class declaration. But, there may be cases where the function keyword will give us more flexibility. Either way, we likely don’t want to use an arrow function.
  3. Do we have a reason to use functions before we’ve written them? Perhaps to create a specific reading order? One that outlines a calculation’s big idea before going into the details? If so, then using function declarations (or other hoisting methods) may help.
  4. Otherwise, a fat arrow function will probably do fine and be more concise.

Of course, these are just my suggestions. You’re free to write code any way that works. You’re even free to write code in a way that doesn’t work, should you so choose.