Если обернуть проверку Playwright в try/catch, провал заглушится: тест пройдёт когда не должен, и баг попадёт в прод. Этот гайд про то, когда обработка ошибок уместна в тестовом коде (сбои настройки, логика повторов, очистка) и когда нет. Плюс finally для гарантированного teardown, кастомные сообщения throw вместо общих ошибок проверок и async-паттерны предотвращающие незамеченные провалы тестов.

Основы: try/catch

Блок try/catch позволяет выполнять код и обрабатывать ошибки без крэша программы:

try {
  // Код который может выбросить ошибку
  const result = JSON.parse('not valid json');
} catch (error) {
  // Выполняется если блок try выбрасывает ошибку
  console.log('Parsing failed:', error.message);
}

Без try/catch вызов JSON.parse('not valid json') крэшнет скрипт с необработанной ошибкой. С try/catch обрабатываешь её корректно.

Переменная error в блоке catch это объект Error со свойствами: error.message (читаемое описание), error.name (тип ошибки: 'SyntaxError', 'TypeError' и др.) и error.stack (полный стек-трейс).

Блок finally

finally выполняется независимо от того была ошибка или нет. Полезен для очистки:

test('загрузка файла с очисткой', async ({ page }) => {
  const tempFile = await createTempFile();
  
  try {
    await page.setInputFiles('[data-testid="upload"]', tempFile);
    await expect(page.locator('[data-testid="upload-success"]')).toBeVisible();
  } catch (error) {
    console.error('Upload test failed:', error.message);
    throw error; // Перебрасываем чтобы тест всё же провалился
  } finally {
    await deleteTempFile(tempFile); // Всегда очищает, даже при провале
  }
});

finally выполняется всегда. Ценно для кода очистки тестов.

async/await и ошибки

В Playwright почти всё async. Ошибки в асинхронном коде работают так же: просто нужен await в правильном месте.

// Без try/catch: необработанный rejection, тест падает с некрасивой ошибкой
async function getUserFromApi() {
  const response = await fetch('/api/user/999');
  const data = await response.json();
  return data;
}

// С try/catch: обработанный rejection, контролируешь сообщение ошибки
async function getUserFromApi() {
  try {
    const response = await fetch('/api/user/999');
    if (!response.ok) {
      throw new Error(`API error: ${response.status}`);
    }
    return await response.json();
  } catch (error) {
    console.error('Failed to fetch user:', error.message);
    throw error; // Перебрасываем если хочешь чтобы вызывающий тоже обработал
  }
}

Выброс собственных ошибок

Можно выбрасывать ошибки намеренно для сигнализации о сбое:

function validateEmail(email: string): void {
  if (!email.includes('@')) {
    throw new Error(`Invalid email format: "${email}"`);
  }
  if (email.length > 254) {
    throw new Error(`Email too long: ${email.length} characters (max 254)`);
  }
}

try {
  validateEmail('not-an-email');
} catch (error) {
  console.log(error.message); // 'Invalid email format: "not-an-email"'
}

В тестах throw используется чтобы провалиться с конкретным сообщением вместо общей ошибки проверки:

async function getAuthToken(request: APIRequestContext): Promise<string> {
  const response = await request.post('/api/auth/login', {
    data: { email: 'test@test.com', password: 'TestPass1' },
  });
  
  if (response.status() !== 200) {
    throw new Error(`Login failed: ${response.status()}, cannot proceed with tests`);
  }
  
  const { token } = await response.json();
  if (!token) {
    throw new Error('Login response missing token field');
  }
  
  return token;
}

Типы ошибок

В JavaScript несколько встроенных типов ошибок:

| Тип ошибки | Когда появляется |

|-----------|----------------|

| Error | Общая ошибка (базовый класс) |

| TypeError | Неверный тип: null.something, вызов не-функции |

| SyntaxError | Невалидный JavaScript или парсинг JSON |

| RangeError | Значение вне диапазона: new Array(-1) |

| ReferenceError | Переменная не существует |

Можно проверять тип чтобы по-разному обрабатывать разные ошибки:

try {
  await doSomething();
} catch (error) {
  if (error instanceof TypeError) {
    console.log('Type error: проверь типы данных');
  } else if (error instanceof SyntaxError) {
    console.log('Syntax error: невалидный JSON или похожее');
  } else {
    throw error; // Неизвестная ошибка: перебрасываем
  }
}

Частые паттерны в тестах Playwright

1. Повтор операции при сбое

async function waitForApiReady(request: APIRequestContext, maxAttempts = 5): Promise<void> {
  for (let i = 0; i < maxAttempts; i++) {
    try {
      const response = await request.get('/api/health');
      if (response.status() === 200) return;
    } catch (error) {
      // API ещё не готово, пробуем снова
    }
    await new Promise(resolve => setTimeout(resolve, 1000));
  }
  throw new Error(`API not ready after ${maxAttempts} attempts`);
}

2. Сбор ошибок без остановки

const errors: string[] = [];

for (const email of testEmails) {
  try {
    await validateEmailField(page, email);
  } catch (error) {
    errors.push(`${email}: ${error.message}`);
  }
}

if (errors.length > 0) {
  throw new Error(`Validation failures:\n${errors.join('\n')}`);
}

3. Кастомный класс ошибки для чистоты кода

class TestDataError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'TestDataError';
  }
}

// Теперь можно отличать ошибки настройки тест-данных от ошибок проверок
try {
  const user = await createTestUser(request);
} catch (error) {
  if (error instanceof TestDataError) {
    console.error('Test setup failed: skipping test');
    test.skip();
  }
  throw error;
}

Ошибки в Playwright: что встретишь

TimeoutError

Самая частая ошибка Playwright. Локатор не стал видимым вовремя:

TimeoutError: locator.click: Timeout 30000ms exceeded.
Locator: [data-testid="submit-button"]

В большинстве случаев это не та JavaScript-ошибка которую нужно перехватывать: Playwright сам обрабатывает её и проваливает тест с понятным сообщением. Но если нужно обработать таймаут корректно (попробовать что-то другое если элемент не появился), можно:

try {
  await page.locator('[data-testid="popup"]').click({ timeout: 2000 });
} catch (error) {
  // Попап не появился за 2 секунды: нормально
  // Продолжаем тест без взаимодействия с попапом
}

Unhandled Promise Rejection

Если вызываешь async-функцию без await и она выбрасывает ошибку, это может стать «необработанным rejection»: тест не падает сразу, и сообщение об ошибке может быть запутанным.

// Плохо: без await, ошибка может быть проглочена
page.goto('/admin');  // Если упадёт, тест может пройти с неверным состоянием

// Хорошо: ошибка всплывает сразу
await page.goto('/admin');

Всегда await для async-операций в тестах.

Когда НЕ использовать try/catch в тестах

Не используй try/catch чтобы скрыть провалы проверок. Если проверка упала, тест должен провалиться:

// Неправильно: глушит провал проверки, тест "проходит" когда не должен
try {
  await expect(page.locator('[data-testid="success"]')).toBeVisible();
} catch (error) {
  console.log('Element not visible but continuing...');
}

// Правильно: даёт провалам проверок проваливать тест
await expect(page.locator('[data-testid="success"]')).toBeVisible();

try/catch в тестах подходит для кода настройки и очистки где сбой не должен маскировать сам тест, для намеренной устойчивости (логика повторов, необязательные взаимодействия) и для создания тест-данных где нужно чёткое кастомное сообщение об ошибке. Не для заглушения проверок.

Краткое резюме

  • try/catch обрабатывает ошибки: код в try выполняется, catch обрабатывает любую выброшенную ошибку
  • finally выполняется всегда: используй для очистки
  • Всегда await для async-операций: необождаемые async-сбои сложно отлаживать
  • throw намеренно когда нужно конкретное сообщение об ошибке
  • Не перехватывай провалы проверок: они должны проваливать тест
  • instanceof позволяет проверить тип ошибки и обрабатывать разные ошибки по-разному
→ See also: JavaScript для QA-инженеров: необходимый минимум для автоматизации | Async/Await простым языком (для тестировщиков, которых сбивают с толку промисы) | Как читать сообщения об ошибках Playwright