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)); // 5The arrow version is shorter because:
- The
functionkeyword is replaced by=>. - A single-expression body doesn't need
{}orreturn— 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)); // 14That'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)); // 10The 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,forEachall 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:
- Top-level named helpers. A reusable, name-worthy function that lives at the top of a file reads better as
function validateResponse(...)thanconst validateResponse = (...) => .... Most teams accept either, but thefunctionform names itself in stack traces, which can help when debugging. - Methods that need their own
this. Arrow functions inheritthisfrom the surrounding scope. If you're writing object methods or class methods that rely onthis, classicfunctionis 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 }returnsundefined, notsomeValue. Either drop the{}(and the result is the expression) or keep them and add an explicitreturn. - Object literals in implicit returns.
() => { id: 1 }doesn't return{ id: 1 }— JavaScript reads{}as a function body andid: 1as a labelled statement. Wrap the object in():() => ({ id: 1 }). - Using arrows where you need
this. In a class method that depends onthis, an arrow function inherits the outerthis(oftenundefinedor the wrong object). Stick to classicfunction(or shorthand methods) inside classes.
🎯 Practice task
Convert and chain. 15-20 minutes.
- Open the
helpers.jsfile from lesson 1's practice task. - Convert
isPassto an arrow function. Confirm it still works. - Add an arrow function
getDuration = (r) => r.durationMs;. (Assume each result has adurationMsfield — make up the data if you don't have it.) - Build an array of 5 result objects, each with
name,status, anddurationMs. - Use
.filter(...).forEach(...)with arrow callbacks to print only the slow failures (status === "fail"ANDdurationMs > 1000). - Run with
node helpers.js. Verify only the slow failures print. - 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. Thenconsole.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.