What’s the difference between named functions and arrow functions in JavaScript?
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.
-
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.
-
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
, orsuper
, and should not be used as methods.- Arrow functions cannot be used as constructors. Calling them with
new
throws aTypeError
. They also don’t have access to thenew.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.
- 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. - Next, we ask, “Do we need to work with the
this
keyword?” If so, we probably want to use method syntax inside aclass
declaration. But, there may be cases where thefunction
keyword will give us more flexibility. Either way, we likely don’t want to use an arrow function. - 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.
- 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.