3. Functions
Package steps into reusable blocks, the building block of every test helper.
Chapter film (about 7m) is coming.
Functions Deep Dive
In plain English. A function is a named, reusable set of steps, like a test case you write once and run with different data. You give it inputs, called parameters (a username and password, say), it does its work, and it can hand back a result. Tests lean on functions constantly: one that logs in, one that formats a date, one you pass to another piece of code to run later. That last kind is a callback: a function handed to another function, which calls it when it is ready. You will also meet arrow functions, a shorter way to write them. A closure is a function that remembers the variables around it where it was created, even after that outer code has finished.
The Power of Functions
In JavaScript a function is a value like any other: you can put it in a variable, pass it to another function, and get one back from a function. (Interviewers call this "first-class functions".) A function does not have to belong to a class. You can define functions using standard declarations (function foo() {}) or expressions (const foo = function() {}). A declaration can be called from a line above the one that defines it (this is called hoisting); an expression cannot.
Arrow Functions
Arrow functions (() => {}) provide a more concise syntax. When the body is a single expression you can drop the braces and the return keyword, and the arrow hands back that expression's value: const add = (a, b) => a + b; returns a + b. They also treat the this keyword differently, which matters once you write classes: a traditional function gets its own this depending on how it is called, while an arrow function uses the this of the code around it. That makes arrow functions the safe choice for callbacks inside a class, with no extra workaround. For now, keep the rule of thumb: use an arrow function for a callback. The JavaScript Classes & OOP lesson explains this itself.
Parameters and Arguments
JavaScript also offers default parameters and rest parameters (...args), making function signatures highly flexible. Default parameters allow you to specify fallback values directly in the signature. The rest parameter syntax captures any number of arguments into a real list (an array), so the array methods you will meet in Array Iteration Methods work on it directly.
Closures
Finally, closures are a fundamental concept where a function retains access to its lexical scope, even after the outer function has returned. The captured variable is live: the closure reads and writes the same binding rather than a copy of its value at the time the outer function returned. Closures are widely used to emulate private variables, create factory functions, or maintain state across asynchronous test steps.
const greet = function(name = 'Guest') {
console.log(`Hello, ${name}!`);
};
const add = (a, b) => a + b;
function sumAll(...numbers) {
// reduce boils a list down to one value; read this line as "add them all up"
return numbers.reduce((acc, curr) => acc + curr, 0);
}
function createCounter() {
let count = 0;
return () => ++count; // ++ before the name adds one first, then returns the new value
}
greet();
greet('Alice');
console.log('Arrow function:', add(5, 3));
console.log('Rest parameters:', sumAll(1, 2, 3, 4));
const counter = createCounter();
console.log('Closure count 1:', counter());
console.log('Closure count 2:', counter());Sign in to track your progress
Parameters: Defaults, Rest and the Options Object
In plain English. Test helpers often need to cope with different inputs: login() for the default user, login('admin') for a named one, or a version where you set every detail yourself. You might expect to write one function per case, all under the same name. JavaScript does not work that way, and this lesson shows what to do instead. Below, a default parameter is a fallback value used when the caller leaves an input out. A rest parameter collects any number of extra inputs into one list. An options object bundles many optional settings into one { } value, so the caller names only the settings they care about. You will write all three in your own helpers, and you will see the options object all over Playwright.
First, the trap. A function name is an ordinary label, so JavaScript cannot pick between two functions that share one. If you define two functions with the same name, the last one defined simply overwrites the first, but only where the language lets you write that at all. At the top level of a CommonJS module or a classic script (the older file style; the Modules lesson explains both styles), duplicate function declarations are allowed and the last wins. Inside an ES module (a file that uses import and export, which the Modules lesson also covers), or inside any block in strict mode (a stricter rule set that ES modules and classes switch on automatically), they are lexically declared, which means they belong to the block they are written in the way let does, and a duplicate is an outright SyntaxError: Identifier 'greet' has already been declared: the file will not even parse. Since Playwright projects routinely use ES modules and TypeScript, the realistic outcome is not a silent overwrite but a build failure.
Why Two Functions With One Name Do Not Work
JavaScript relies on dynamic typing, and the runtime engine does not inspect argument types or counts to route function calls. Every JS function can accept any number of arguments, regardless of how many it was defined with. Extra arguments are ignored (or captured), and missing arguments are set to undefined.
function greet(name) { return 'Hello ' + name; } function greet(name, age) { return name + ' is ' + age; } console.log(greet('Alice')); // 'Alice is undefined' - second declaration won // The SAME two lines inside an ES module (.mjs, or .js with "type": "module"): // SyntaxError: Identifier 'greet' has already been declared
Pattern 1: Argument Checking
The most traditional way to simulate overloading in JS is by inspecting the arguments you were handed — their count or their typeof. Older code reads the array-like arguments object for this; prefer a rest parameter (...args), which gives you a real array and, unlike arguments, is also available inside arrow functions (arrow functions have no arguments binding of their own).
function processInput(input) { if (typeof input === 'string') { return 'String: ' + input; } else if (typeof input === 'number') { return 'Number: ' + (input * 2); } } console.log(processInput('test')); // String: test console.log(processInput(5)); // Number: 10
Pattern 2: Default Parameters
Instead of writing several functions to cover the cases where a parameter is missing, JS lets you assign default values to parameters directly in the signature. If a parameter is omitted or passed as undefined, the default is used.
function connect(host = 'localhost', port = 8080) { return `Connecting to ${host}:${port}`; } console.log(connect()); // Connecting to localhost:8080 console.log(connect('db.local', 5432)); // Connecting to db.local:5432
Pattern 3: The Options Object
When functions require many optional arguments, relying on positional defaults becomes cumbersome. The options object pattern replaces long parameter lists with a single configuration object, heavily used in Playwright APIs. In the signature below, { headless = true, slowMo = 0 } unpacks the named values out of the object the caller passes, with a default for each; the Objects & Destructuring lesson covers unpacking in full. The = {} at the end lets the caller pass nothing at all.
function launchBrowser({ headless = true, slowMo = 0 } = {}) { // Destructuring defaults apply when the property is missing or `undefined`. // Do NOT write `options.slowMo || 0` here: `||` also swallows a deliberate 0, // and `headless: false` would be thrown away the same way. Use `??` if you // need a fallback expression rather than a destructuring default. // (`??` is covered in the JSON & Data Manipulation lesson.) return `Launched headless=${headless}, slowMo=${slowMo}`; } console.log(launchBrowser({ headless: false })); // Launched headless=false, slowMo=0
Pattern 4: Rest Parameters
For variadic functions — functions that take an arbitrary number of arguments — JavaScript uses the rest parameter syntax ...args. This collects all remaining arguments into an array.
function sumAll(...numbers) { // reduce boils a list down to one value; read it as "add them all up" return numbers.reduce((acc, curr) => acc + curr, 0); } console.log(sumAll(1, 2, 3, 4)); // 10
Default parameters, rest parameters and the options object cover between them everything overloading is normally used for.
function initApp({ port = 3000, host = 'localhost' } = {}) {
console.log(`Starting on ${host}:${port}`);
}
initApp();
initApp({ port: 8080 });
function dynamicAdd(...args) {
// reduce boils a list down to one value; read it as "add them all up"
return args.reduce((sum, n) => sum + n, 0);
}
console.log('Sum:', dynamicAdd(10, 20, 30));Sign in to track your progress
Return Types & Patterns
In plain English. When a function finishes its job, it can hand a result back to whoever called it. That result is the return value, and the word return is how you send it back. Your test helpers live on this: a helper that builds test data, reads a price off a page, or picks a user returns the thing your test then checks. JavaScript does not police what comes back, so one function can return text on one path and nothing at all on another, and that is where bugs hide. Below, undefined is JavaScript's word for "no value here".
A JavaScript function has no declared return type and nothing enforces what comes out of it. A single function can return a string, a number, an object, or another function, and which one it returns can depend on runtime conditions.
Implicit Undefined Returns
If a function in JavaScript doesn't explicitly return a value, it returns undefined. If you simply write return; to exit early, it evaluates to undefined as well.
function doNothing() { // No return statement } console.log(doNothing()); // undefined
Arrow Functions and Implicit Returns
Arrow functions in JavaScript introduce a concise syntax for implicit returns. If an arrow function has a single expression and no curly braces { ... }, the result of that expression is returned automatically. If you add curly braces, you must explicitly use the return keyword, otherwise it behaves like a normal function and returns undefined.
const add = (a, b) => a + b; console.log(add(2, 3)); // 5 // Explicit Return required with braces const multiply = (a, b) => { return a * b; };
Returning Objects from Arrow Functions
When returning an object literal from an implicit arrow function, you must wrap the object in parentheses ({ ... }). Without parentheses, JavaScript interprets the curly braces as the start of the function body, leading to syntax errors or undefined returns.
const createUser = (name) => ({ id: 1, name: name }); console.log(createUser('Alice')); // { id: 1, name: 'Alice' }
Returning Multiple Values
To return more than one value, return an Array or an Object and unpack it at the call site with destructuring syntax. No wrapper type is needed. Read const { x, y } = getCoordinates() as "take x and y out of the result"; the Objects & Destructuring lesson covers it in full.
function getCoordinates() { return { x: 10, y: 20 }; } const { x, y } = getCoordinates(); console.log(`x: ${x}, y: ${y}`);
Having no enforced return type makes utility functions flexible, and it puts the discipline on you: a function that returns different shapes on different paths produces bugs that only appear at runtime, at whichever call site got the shape it did not expect.
One more kind of return value waits in the async chapter. A function marked async hands back a Promise, a value that will only arrive later; the Callbacks & Promises lesson covers it.
const add = (a, b) => a + b;
console.log(add(2, 3));
const getUser = (name) => ({ id: 123, name });
console.log(getUser('Bob'));
function getMultiple() {
return [42, 'Answer'];
}
const [num, str] = getMultiple();
console.log(num, str);- fixThe return that returned nothing
- mcqJavaScript — quiz bank (1 question)🔒 standard
Sign in to track your progress
Higher-Order Functions
In plain English. In JavaScript a function is a value, just like a number or a piece of text. You can store it, pass it into another function, or get one back as a result. A higher-order function is a function that does one of those: it takes a function in, or hands one out. You will meet some soon: the .map() and .filter() methods that work on lists are higher-order functions. Testers use this to add behaviour around an action without rewriting the action: log every step, time it, or retry a flaky one. A callback is the function you pass in, for the receiving function to call when it is ready. Every Playwright test you write is a callback, handed to test() to run.
A higher-order function is any function that either takes one or more functions as arguments, or returns a function as its result. In JavaScript this needs no special type or wrapper: functions are first-class values, so a function can be stored in a variable, handed to another function, and returned like any other value.
Functions as Arguments (Callbacks)
The most common use of higher-order functions in JS is passing callbacks. Arrays (ordered lists written in square brackets; the next chapter covers them) have built-in higher-order functions like .map(), .filter(), and .reduce() that apply a callback function to their elements. This functional programming style is heavily favored in modern JS.
const numbers = [1, 2, 3, 4, 5]; const evens = numbers.filter(n => n % 2 === 0); const doubled = evens.map(n => n * 2); console.log(doubled); // [4, 8]
The check n % 2 === 0 uses %, which gives the remainder after dividing: it is 0 exactly when n is even.
Function Factories (Returning Functions)
A function factory is a function that generates and returns another function, often locking in some configuration. Because JS maintains scope closures, the returned function remembers the environment in which it was created.
function createMultiplier(multiplier) { return function(value) { return value * multiplier; }; } const double = createMultiplier(2); console.log(double(10)); // 20
Decorators and Wrappers
You can wrap an existing function with additional behavior (like logging, timing, or retrying) without modifying the original function. This is a common pattern in automation frameworks. The wrapper collects whatever arguments it is called with into args with a rest parameter (from Parameters: Defaults, Rest and the Options Object), then fn(...args) spreads that list back out, so fn receives them one by one, exactly as they arrived.
function withLogging(fn) { return function(...args) { console.log(`Calling function with args: ${args}`); const result = fn(...args); console.log(`Result: ${result}`); return result; }; } const loggedAdd = withLogging((a, b) => a + b); loggedAdd(3, 4);
Retrying a Flaky Step
In automation, flakes occur, and a wrapper that tries an action again is the classic fix. It needs async and await, which the async chapter teaches, so you will write it there, in exercise 2 of Practice and interview questions: Asynchronous JavaScript & Modern ES6+.
Playwright Use Cases
Playwright uses them heavily. A test is written as test('name', async ({ page }) => { ... }): you pass test a callback to run, and you will write your first one in Your First Playwright Test. (async is explained in the async chapter.) Later, in the Fixtures chapter, Playwright's own test.extend(), another higher-order function, builds customised versions of test.
function createGreeter(greeting) {
return (name) => `${greeting}, ${name}!`;
}
const sayHi = createGreeter('Hi');
console.log(sayHi('Alice'));
const nums = [1, 2, 3, 4];
const squared = nums.map(x => x * x);
console.log(squared);
function timeIt(fn) {
return (...args) => {
const start = Date.now(); // the current time, in milliseconds
const res = fn(...args);
console.log(`Execution took ${Date.now() - start}ms`);
return res;
};
}
const fastFunc = timeIt(() => 42);
fastFunc();Sign in to track your progress
Practice and interview questions: Functions
This is the practice set for the Functions chapter. Each exercise asks you to wrap a small job in a function, so you can call it again with different input, the way a test helper is used. The inputs are the same kind of values you handled in the first two chapters: emails, status codes and prices read off a page.
Write each function, then call it with a normal input and with an awkward one, such as an empty string. Print the result with console.log to check it. If a function prints undefined, look for a missing return.
Coding Exercises
-
Extract the Domain (Easy) Problem: Write a function
extractDomain(email)that takes an email string and returns the part after the@. If the email has no@, returnnull. Hint:includes('@')checks for the@.email.indexOf('@')gives its position, andslicefrom one past that position gives the rest. -
Status Code Labels (Easy) Problem: Write a function
checkStatus(code)that returns"Success"for 200-299,"Redirect"for 300-399,"Client Error"for 400-499 and"Server Error"for 500-599. Return"Unknown"otherwise. Hint: Anif / else ifchain where each branch has its ownreturn, or aswitchstatement withtrueas the expression. -
Parse a UI Price (Medium) Problem: Write a function
parsePrice(text)that turns"Total: $1,234.56 USD"into the number1234.56. Hint: Remove the pieces that are not part of the number withreplace, then useparseFloat(). -
Browser Options (Easy) Problem: Write a function
describeLaunch({ browser = 'chromium', headless = true } = {})that returns a text such aschromium, headless: true. It must work when called with no argument at all, and with only one of the two settings. Hint: The defaults in the signature do the work. Test it withdescribeLaunch(),describeLaunch({ headless: false })anddescribeLaunch({ browser: 'firefox' }).
Interview Questions
-
What happens if you omit the
returnstatement in a function? Answer: The function implicitly returnsundefined. -
Why does an arrow function written with curly braces return
undefinedwhen you forgetreturn? Answer: Without braces, an arrow function returns its single expression automatically. With braces, the body is an ordinary block, so you must writereturnyourself. -
What is hoisting, and does it apply to
letandconst? Answer:varis hoisted with the valueundefined, and function declarations are hoisted whole, so you can call them before the line that defines them.letandconstare hoisted too, but they stay in the Temporal Dead Zone until the line that declares them runs. Reading one earlier throws aReferenceError. -
What is the difference between a default parameter and a rest parameter? Answer: A default parameter gives a fallback value for an input the caller left out:
function connect(host = 'localhost'). A rest parameter (...args) collects any number of extra inputs into a real list:function sumAll(...numbers).
// Exercise 1: Extract the Domain
function extractDomain(email) {
if (!email.includes('@')) {
return null;
}
return email.split('@')[1];
}
console.log(extractDomain('tester@playwright.dev')); // Expected: playwright.dev
console.log(extractDomain('invalid-email')); // Expected: null
// Exercise 2: Status Code Labels
function checkStatus(code) {
if (code >= 200 && code <= 299) return 'Success';
if (code >= 300 && code <= 399) return 'Redirect';
if (code >= 400 && code <= 499) return 'Client Error';
if (code >= 500 && code <= 599) return 'Server Error';
return 'Unknown';
}
console.log(checkStatus(204), checkStatus(404), checkStatus(42)); // Success Client Error Unknown
// Exercise 3: Parse a UI Price
function parsePrice(text) {
return parseFloat(text.replace('Total: $', '').replace(' USD', '').replace(',', ''));
}
console.log(parsePrice('Total: $1,234.56 USD')); // Expected: 1234.56
// Exercise 4: Browser Options
function describeLaunch({ browser = 'chromium', headless = true } = {}) {
return `${browser}, headless: ${headless}`;
}
console.log(describeLaunch()); // chromium, headless: true
console.log(describeLaunch({ headless: false })); // chromium, headless: false
console.log(describeLaunch({ browser: 'firefox' })); // firefox, headless: trueSign in to track your progress
Practice
3 exercises attached to this chapter, about 8m.
- fixZero is not missing
- fixThe return that returned nothing
- mcqJavaScript — quiz bank (1 question)🔒 standard