Lesson 9 of 14
Lesson 09 — Function styles for QA automation
Title: Function Declarations, Function Expressions, and Arrow Functions
Description: In automation code, there are three common ways to write functions: function declarations, function expressions stored in const, and arrow functions. This lesson explains the differences in syntax, when each one is available, and how they handle this. It also covers concise syntax like implicit return in arrow functions.
Why it matters for QA: Functions are used everywhere in test code: hooks, helpers, factories, matchers, and retry logic. Understanding the differences helps avoid bugs like this binding issues or “cannot access before initialization” errors, and keeps code style consistent across the test suite.
1. Function declaration
A function declaration creates a function with a name that you can use later in the code.
function buildTestName(feature, scenario) {
return `${feature}: ${scenario}`;
}
const testName = buildTestName("Login", "valid user can sign in");
console.log(testName);
Use function declarations for reusable functions in your program. (The function name also helps when reading error messages and stack traces.)
2. Function declarations are hoisted
You can call a function before it appears in the code.
console.log(isSuccessStatus(201)); // true
function isSuccessStatus(statusCode) {
return statusCode >= 200 && statusCode < 300;
}
This works because JavaScript creates function declarations in memory before running the code.
3. Function expression
A function expression means you create a function and save it in a variable.
const normalizeEmail = function (email) {
return email.trim().toLowerCase();
};
console.log(normalizeEmail(" QA@Example.COM "));
When you use const, you cannot accidentally replace this function with something else later.
4. Function expressions are not usable before initialization
This code will fail:
// ReferenceError: Cannot access 'buildUserId' before initialization
console.log(buildUserId(42));
const buildUserId = function (id) {
return `user-${id}`;
};
You get a ReferenceError because the variable exists, but you cannot use it before the line where it is defined runs.
This happens because const and let are in a temporary “unusable” state before they are initialized. This is called the temporal dead zone.
5. Arrow function with block body
Arrow functions are often used for small helper functions and callbacks.
const isAdminUser = (user) => {
return user.role === "admin";
};
const user = { email: "admin@example.com", role: "admin" };
console.log(isAdminUser(user)); // true
Use a block body ({}) when you need more than one step or when you want to use return explicitly.
6. Arrow function with implicit return
If a function is only one expression, you can skip {} and return.
const getErrorMessage = (fieldName) => `${fieldName} is required`;
console.log(getErrorMessage("Email"));
This style is shorter and is often used with array methods:
const users = [
{ email: "admin@example.com" },
{ email: "viewer@example.com" },
];
const emails = users.map((user) => user.email);
console.log(emails);
7. Returning an object from an arrow function
When you return an object in a short arrow function, you must wrap it in parentheses.
const createUser = (email) => ({
email,
role: "viewer",
isActive: true,
});
console.log(createUser("qa@example.com"));
Without the parentheses, JavaScript thinks {} is a function block, not an object.
8. Callback example in QA code
Array methods often use arrow functions as callbacks.
const testResults = [
{ name: "login", status: "passed" },
{ name: "checkout", status: "failed" },
{ name: "profile", status: "passed" },
];
const failedResults = testResults.filter((result) => {
return result.status === "failed";
});
console.log(failedResults);
Short version:
const failedNames = testResults
.filter((result) => result.status === "failed")
.map((result) => result.name);
console.log(failedNames);
Try to keep method chains readable. If it becomes hard to follow, split it into named variables.
9. this binding: conventional vs arrow functions
Arrow functions don’t have their own this. They use this from the surrounding scope. Regular functions get this based on how they are called.
Simple idea:
- Regular function →
thisdepends on how you call it - Arrow function →
thisis taken from where it was created
const loginPage = {
name: "Login page",
open: function () {
console.log(`Open ${this.name}`);
},
};
loginPage.open();
In this example, this.name correctly refers to "Login page" because open is a normal function method.
Don’t use arrow functions for object methods when you need this, because they won’t behave as expected.
10. Choosing the right style
| Style | Best use |
|---|---|
| Function declaration | Named helper at the top level |
| Function expression | Function stored in a variable |
| Arrow function | Callbacks, small transformations, simple helpers |
| Arrow implicit return | One simple expression |
| Arrow block body | Multiple steps or when you need return |
Try not to mix styles randomly in the same file. Consistency helps readers focus on what the code does, not how it is written.
11. Common mistakes
Mistake 1: forgetting return
const isPositive = (value) => {
value > 0;
};
console.log(isPositive(5)); // undefined
Fix:
const isPositive = (value) => {
return value > 0;
};
Mistake 2: calling a const function too early
// Wrong: this line runs before the helper is initialized.
// console.log(formatId(7));
const formatId = (id) => `id-${id}`;
console.log(formatId(7));
Mistake 3: long anonymous callbacks
const activeAdminUsers = users.filter((user) => {
const isAdmin = user.role === "admin";
const isActive = user.isActive === true;
return isAdmin && isActive;
});
This is still fine. But if the logic becomes more complex, it’s better to move it into a named function.
const isActiveAdmin = (user) => {
return user.role === "admin" && user.isActive === true;
};
const activeAdminUsers = users.filter(isActiveAdmin);
Official docs
Quick recap
| Topic | Takeaway for QA automation |
|---|---|
| Declaration | Good for named helpers and is hoisted |
| Expression | Function stored in a variable |
| Arrow function | Great for callbacks and short helpers |
| Implicit return | Works only for one expression |
| Object return | Use parentheses: () => ({ value: 1 }) |
this | Don’t use arrow functions inside objects when you need this |
Suggested exercises
- Write a function declaration named
isSuccessStatus(statusCode). - Write a function expression named
normalizeEmail. - Rewrite
normalizeEmailas an arrow function. - Use
mapwith an arrow function to extract user emails. - Write an arrow function that returns an object literal.
Homework
Short tasks (about 20-25 minutes). Click a task title to reveal the prompt.
Task 1: declaration helper
Write function buildTestTitle(feature, scenario) that returns a string in this format: "Feature - Scenario". Call it with "Login" and "valid credentials".
Task 2: expression helper
Create const normalizeEmail = function (email) { ... }. It must trim spaces and convert the email to lowercase.
Task 3: arrow rewrite
Rewrite normalizeEmail as an arrow function. Use a block body with explicit return.
Task 4: implicit return
Write const isFailed = (result) => result.status === "failed";. Test it with { status: "failed" } and { status: "passed" }.
Task 5: return object
Write const createViewer = (email) => ({ email, role: "viewer" });. Explain why the parentheses around the object are required.
Task 6: filter callback
Given an array of test results, use filter with an arrow callback to keep only failed tests.
Task 7: hoisting check
Call a function declaration before its definition and confirm it works. Then try the same with const helper = function () {} and explain the error.