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.

javascriptjavascript
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.

javascriptjavascript
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.

javascriptjavascript
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:

javascriptjavascript
// 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.

javascriptjavascript
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.

javascriptjavascript
const getErrorMessage = (fieldName) => `${fieldName} is required`;

console.log(getErrorMessage("Email"));

This style is shorter and is often used with array methods:

javascriptjavascript
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.

javascriptjavascript
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.

javascriptjavascript
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:

javascriptjavascript
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 → this depends on how you call it
  • Arrow function → this is taken from where it was created
javascriptjavascript
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

StyleBest use
Function declarationNamed helper at the top level
Function expressionFunction stored in a variable
Arrow functionCallbacks, small transformations, simple helpers
Arrow implicit returnOne simple expression
Arrow block bodyMultiple 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

javascriptjavascript
const isPositive = (value) => {
  value > 0;
};

console.log(isPositive(5)); // undefined

Fix:

javascriptjavascript
const isPositive = (value) => {
  return value > 0;
};

Mistake 2: calling a const function too early

javascriptjavascript
// 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

javascriptjavascript
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.

javascriptjavascript
const isActiveAdmin = (user) => {
  return user.role === "admin" && user.isActive === true;
};

const activeAdminUsers = users.filter(isActiveAdmin);

Official docs


Quick recap

TopicTakeaway for QA automation
DeclarationGood for named helpers and is hoisted
ExpressionFunction stored in a variable
Arrow functionGreat for callbacks and short helpers
Implicit returnWorks only for one expression
Object returnUse parentheses: () => ({ value: 1 })
thisDon’t use arrow functions inside objects when you need this

Suggested exercises

  1. Write a function declaration named isSuccessStatus(statusCode).
  2. Write a function expression named normalizeEmail.
  3. Rewrite normalizeEmail as an arrow function.
  4. Use map with an arrow function to extract user emails.
  5. 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.