Arrow Functions and When to Use Them

7 min read

Modern JavaScript test code is full of short functions written with =>. They're called arrow functions, and they're not just shorter — they read more naturally as callbacks and they're the de-facto syntax for the array methods you'll meet in the next chapter. This lesson covers the syntax, the four shapes you'll encounter, and the cases where a classic function is still the right call.

The basic shape

A regular function and its arrow equivalent, side by side:

// Classic
function add(a, b) {
  return a + b;
}
 
// Arrow
const add2 = (a, b) => a + b;
 
console.log(add(2, 3));   // 5
console.log(add2(2, 3));  // 5

The arrow version is shorter because:

  • The function keyword is replaced by =>.
  • A single-expression body doesn't need {} or return — the result of the expression is returned automatically.

It's also assigned to a const, which is the conventional way to give an arrow function a name.

Single parameter — parentheses optional

When there's exactly one parameter, you can drop the parentheses:

const double = n => n * 2;
console.log(double(7));   // 14

That's a personal-style choice. Many teams (and Prettier by default) keep the parentheses for consistency:

const double = (n) => n * 2;

Either is fine; pick one and stick with it.

No parameters — empty parentheses required

When there are zero parameters, you must include the empty ():

const getTimestamp = () => Date.now();
console.log(getTimestamp());

Output (yours will differ):

1715000000000

You can't write => Date.now() on its own — the () tells JavaScript the parameter list is empty.

Multi-line — curly braces and explicit return

For anything more than a single expression, wrap the body in {} and use the return keyword the same way you would in a classic function:

const validate = (response) => {
  if (response.statusCode !== 200) return "❌ bad status";
  if (response.time >= 2000)       return "❌ slow";
  return "✅ pass";
};
 
console.log(validate({ statusCode: 200, time: 1500 }));

Output:

✅ pass

The trade-off: once you've added {}, you give up the implicit return — () => { 5 } returns undefined, not 5. You have to write () => { return 5; } explicitly.

Implicit vs explicit return

Two arrow shapes look very similar but behave very differently:

const a = (x) => x * 2;              // implicit return — returns x * 2
const b = (x) => { x * 2; };         // explicit body, no return — returns undefined!
const c = (x) => { return x * 2; };  // explicit body, with return — returns x * 2
 
console.log(a(5));  // 10
console.log(b(5));  // undefined  ← surprise
console.log(c(5));  // 10

The rule: if the body is a single expression, drop the braces. If you need {} for any reason, you also need return.

When to use arrow functions

Arrows are the natural fit for:

  • Array method callbacks. filter, map, find, forEach all take a function. Arrows make those one-liners.
  • Promise chains. fetch(url).then(res => res.json()).then(data => ...).
  • Short utilities. A two-line helper passed inline.
  • Event handlers in modern frameworks. React, Vue, etc. lean heavily on arrow callbacks.

A real example — filtering test results down to the failures, then printing each one:

const results = [
  { name: "login",    status: "pass" },
  { name: "checkout", status: "fail" },
  { name: "search",   status: "fail" },
  { name: "logout",   status: "pass" }
];
 
const failedTests = results.filter(r => r.status === "fail");
failedTests.forEach(t => console.log(`❌ ${t.name}`));

Output:

❌ checkout
❌ search

Two arrow functions, four lines of code, complete behaviour. The same logic with classic function expressions reads twice as long for no extra clarity.

Chained array methods

Arrows really shine when you chain. A common QA pattern: take a list of users, keep the active ones, extract their emails, send each one a notification.

const users = [
  { name: "Alice", active: true,  email: "alice@x.com" },
  { name: "Bob",   active: false, email: "bob@x.com" },
  { name: "Carol", active: true,  email: "carol@x.com" }
];
 
users
  .filter(u => u.active)
  .map(u => u.email)
  .forEach(email => console.log(`Notifying ${email}`));

Output:

Notifying alice@x.com
Notifying carol@x.com

Each arrow is named once, used once, and stays close to where it's needed. Chapter 4 covers filter, map, and friends in depth — for now, just notice how readable the chain is.

When to use a regular function

Two reasons to reach for the function keyword instead:

  1. Top-level named helpers. A reusable, name-worthy function that lives at the top of a file reads better as function validateResponse(...) than const validateResponse = (...) => .... Most teams accept either, but the function form names itself in stack traces, which can help when debugging.
  2. Methods that need their own this. Arrow functions inherit this from the surrounding scope. If you're writing object methods or class methods that rely on this, classic function is the safer default. Outside classes, arrows almost always do what you expect, so this rule rarely bites in test code.

Same function, two shapes

Classic vs arrow — same logic, three QA helpers

Classic function

  • function isPass(r) { return r.status === "pass"; }

  • function double(n) { return n * 2; }

  • function ts() { return Date.now(); }

  • Verbose, but named in stack traces

Arrow function

  • const isPass = r => r.status === "pass";

  • const double = n => n * 2;

  • const ts = () => Date.now();

  • Concise, ideal for callbacks

Both columns describe the exact same behaviour — pick the shape that reads better in context.

⚠️ Common mistakes

  • Implicit return with curly braces. () => { someValue } returns undefined, not someValue. Either drop the {} (and the result is the expression) or keep them and add an explicit return.
  • Object literals in implicit returns. () => { id: 1 } doesn't return { id: 1 } — JavaScript reads {} as a function body and id: 1 as a labelled statement. Wrap the object in (): () => ({ id: 1 }).
  • Using arrows where you need this. In a class method that depends on this, an arrow function inherits the outer this (often undefined or the wrong object). Stick to classic function (or shorthand methods) inside classes.

🎯 Practice task

Convert and chain. 15-20 minutes.

  1. Open the helpers.js file from lesson 1's practice task.
  2. Convert isPass to an arrow function. Confirm it still works.
  3. Add an arrow function getDuration = (r) => r.durationMs;. (Assume each result has a durationMs field — make up the data if you don't have it.)
  4. Build an array of 5 result objects, each with name, status, and durationMs.
  5. Use .filter(...).forEach(...) with arrow callbacks to print only the slow failures (status === "fail" AND durationMs > 1000).
  6. Run with node helpers.js. Verify only the slow failures print.
  7. Stretch: rewrite the chain so the result is an array assigned to const slowFails = results.filter(...).map(...) — for example, an array of the slow-fail names. Then console.log(slowFails).

The next lesson covers scope — where in your code each variable can be seen — and the rules behind let, const, and the legacy var.

// tip to track lessons you complete and pick up where you left off across devices.