level 01 / functions-and-async

Functions & Async/Await

Function types, arrow functions, and the async/await patterns that every Playwright test uses — with real examples from test fixtures and page objects.

Function signatures

TypeScript functions declare the types of their parameters and return value:

// Named function with typed params and return
function validateAge(age: number): boolean {
  return age >= 18 && age <= 65;
}

// Arrow function — same semantics, shorter syntax
const validateAge = (age: number): boolean => age >= 18 && age <= 65;

// Optional parameter (must come after required params)
function login(username: string, password?: string): void {
  // password may be undefined
}

// Default parameter
function navigate(url: string, timeout: number = 30_000): void {
  // timeout defaults to 30_000 if not provided
}

// Rest parameter — collects remaining args into an array
function logSteps(...steps: string[]): void {
  steps.forEach((s, i) => console.log(`${i + 1}. ${s}`));
}

Arrow functions

// Arrow function — concise for callbacks
const double = (n: number): number => n * 2;

// No return type needed when inferred
const greet = (name: string) => `Hello, ${name}`;

// No parens needed for single parameter (style preference — parens are safer)
const square = (n: number) => n * n;

// Multi-line arrow function needs curly braces and explicit return
const processResult = (status: string): string => {
  if (status === 'pass') return '✓';
  if (status === 'fail') return '✕';
  return '○';
};

Promises and async/await

Playwright is entirely async. Every page interaction returns a Promise.

// Promise<T> — represents a value that will be available later
function fetchUser(id: number): Promise<{ name: string; email: string }> {
  return fetch(`/api/users/${id}`).then(r => r.json());
}

// async/await — syntactic sugar over Promises
async function fetchUser(id: number): Promise<{ name: string; email: string }> {
  const response = await fetch(`/api/users/${id}`);
  return response.json();
}

// In Playwright — every test callback is async
test('user can log in', async ({ page }) => {
  await page.goto('https://www.saucedemo.com');
  await page.getByPlaceholder('Username').fill('standard_user');
  await page.getByPlaceholder('Password').fill('secret_sauce');
  await page.getByRole('button', { name: 'Login' }).click();
  await expect(page.getByText('Products')).toBeVisible();
});
Key insight: Every await in a Playwright test is a checkpoint. If you forget await, the assertion runs before the action completes — a common source of flakiness.

Error handling in async code

// try/catch wraps awaited operations
async function safeNavigate(page: Page, url: string): Promise<boolean> {
  try {
    await page.goto(url, { timeout: 5_000 });
    return true;
  } catch (error) {
    console.error(`Navigation failed: ${url}`, error);
    return false;
  }
}

// Parallel async operations
async function runParallel(page: Page): Promise<void> {
  // await both concurrently — faster than sequential awaits
  const [title, isVisible] = await Promise.all([
    page.title(),
    page.getByRole('button', { name: 'Login' }).isVisible(),
  ]);
  console.log(title, isVisible);
}

Void vs never vs undefined

// void — function returns nothing meaningful (side effects only)
function logResult(msg: string): void {
  console.log(msg);
}

// never — function never returns (throws or loops forever)
function fail(message: string): never {
  throw new Error(message);
}

// undefined — an explicit missing value
function findUser(id: number): string | undefined {
  if (id < 0) return undefined;
  return `user-${id}`;
}

Try it: async patterns

Interview questions