Envolver una aserción de Playwright en try/catch silencia el fallo: el test pasa cuando no debería, y el bug llega a producción. Esta guía cubre cuándo el manejo de errores pertenece al código de tests (fallos de setup, lógica de reintentos, limpieza) y cuándo no, más finally para garantizar el teardown, mensajes throw personalizados que reemplazan los errores genéricos de aserciones, y los patrones async que evitan que los rechazos no manejados oculten fallos en los tests.
Lo básico: try/catch
Un bloque try/catch permite ejecutar código y manejar errores sin crashear el programa:
try {
// Código que podría lanzar un error
const resultado = JSON.parse('json inválido');
} catch (error) {
// Esto corre si el bloque try lanza un error
console.log('El parseo falló:', error.message);
}Sin try/catch, JSON.parse('json inválido') crashearía el script con un error no manejado. Con try/catch, lo manejas de forma controlada.
La variable error en el bloque catch es el objeto Error, que tiene error.message (descripción legible para humanos), error.name (tipo de error: 'SyntaxError', 'TypeError', etc.) y error.stack (stack trace completo).
El bloque finally
finally corre sin importar si hubo un error o no. Es útil para limpieza:
test('carga de archivo con limpieza', async ({ page }) => {
const archivoTemp = await createTempFile();
try {
await page.setInputFiles('[data-testid="upload"]', archivoTemp);
await expect(page.locator('[data-testid="upload-success"]')).toBeVisible();
} catch (error) {
console.error('El test de carga falló:', error.message);
throw error; // Re-lanzar para que el test siga fallando
} finally {
await deleteTempFile(archivoTemp); // Siempre limpia, incluso si hay fallo
}
});finally siempre corre. Es valioso para el código de limpieza de tests.
async/await y errores
En Playwright, casi todo es async. Los errores en código async funcionan igual: solo necesitas await en el lugar correcto:
// Sin try/catch: rechazo no manejado = el test falla con un error feo
async function getUserFromApi() {
const response = await fetch('/api/user/999');
const data = await response.json();
return data;
}
// Con try/catch: rechazo manejado = vos controlás el mensaje de error
async function getUserFromApi() {
try {
const response = await fetch('/api/user/999');
if (!response.ok) {
throw new Error(`Error de API: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('No se pudo obtener el usuario:', error.message);
throw error; // Re-lanzar si querés que el llamador también lo maneje
}
}Lanzar tus propios errores
Puedes lanzar errores intencionalmente para señalar un fallo:
function validateEmail(email: string): void {
if (!email.includes('@')) {
throw new Error(`Formato de email inválido: "${email}"`);
}
if (email.length > 254) {
throw new Error(`Email demasiado largo: ${email.length} caracteres (máximo 254)`);
}
}
try {
validateEmail('no-es-un-email');
} catch (error) {
console.log(error.message); // 'Formato de email inválido: "no-es-un-email"'
}En los tests, lanzar errores es cómo fallas con un mensaje específico en lugar de un fallo genérico de aserción:
async function getAuthToken(request: APIRequestContext): Promise<string> {
const response = await request.post('/api/auth/login', {
data: { email: 'test@test.com', password: 'PassTest1' },
});
if (response.status() !== 200) {
throw new Error(`Login fallido: ${response.status()}. No se puede continuar con los tests`);
}
const { token } = await response.json();
if (!token) {
throw new Error('La respuesta del login no tiene el campo token');
}
return token;
}Tipos de errores
JavaScript tiene varios tipos de error integrados:
| Tipo de error | Cuándo aparece |
|--------------|----------------|
| Error | Error genérico (clase base) |
| TypeError | Tipo incorrecto: null.algo, llamar algo que no es función |
| SyntaxError | JavaScript inválido o parseo de JSON |
| RangeError | Valor fuera de rango: new Array(-1) |
| ReferenceError | La variable no existe |
Puedes verificar el tipo para manejar errores distintos de forma diferente:
try {
await doSomething();
} catch (error) {
if (error instanceof TypeError) {
console.log('Error de tipo: verificá los tipos de tus datos');
} else if (error instanceof SyntaxError) {
console.log('Error de sintaxis: JSON inválido o similar');
} else {
throw error; // Error desconocido: re-lanzarlo
}
}Patrones comunes en tests de Playwright
1. Reintentar una operación ante un fallo
async function waitForApiReady(request: APIRequestContext, maxIntentos = 5): Promise<void> {
for (let i = 0; i < maxIntentos; i++) {
try {
const response = await request.get('/api/health');
if (response.status() === 200) return;
} catch (error) {
// La API todavía no está lista, reintentar
}
await new Promise(resolve => setTimeout(resolve, 1000));
}
throw new Error(`La API no está lista después de ${maxIntentos} intentos`);
}2. Recolectar errores sin detener la ejecución
const errores: string[] = [];
for (const email of emailsDePrueba) {
try {
await validateEmailField(page, email);
} catch (error) {
errores.push(`${email}: ${error.message}`);
}
}
if (errores.length > 0) {
throw new Error(`Fallos de validación:\n${errores.join('\n')}`);
}3. Clase de error personalizada para código más limpio
class TestDataError extends Error {
constructor(message: string) {
super(message);
this.name = 'TestDataError';
}
}
// Ahora podés distinguir errores de configuración de datos de prueba
// de errores de aserción del test
try {
const usuario = await createTestUser(request);
} catch (error) {
if (error instanceof TestDataError) {
console.error('El setup del test falló: saltando el test');
test.skip();
}
throw error;
}Errores en Playwright: lo que vas a ver
TimeoutError
El error más común en Playwright. Un locator no se volvió visible a tiempo:
TimeoutError: locator.click: Timeout 30000ms exceeded.
Locator: [data-testid="submit-button"]En la mayoría de los casos no es un error de JavaScript que necesites capturar: Playwright lo maneja y falla el test con un mensaje claro. Pero si querés manejar un timeout de forma controlada (intentar otra cosa si un elemento no aparece), podés hacerlo:
try {
await page.locator('[data-testid="popup"]').click({ timeout: 2000 });
} catch (error) {
// El popup no apareció en 2 segundos: está bien
// Continuar el test sin interactuar con el popup
}Rechazo de promesa no manejado
Si llamás una función async sin await y lanza un error, puede ser un "unhandled rejection": no falla el test inmediatamente y el mensaje de error puede ser confuso.
// Mal: sin await, el error puede silenciarse
page.goto('/admin'); // Si falla, el test podría pasar con un estado incorrecto
// Bien: el error aparece inmediatamente
await page.goto('/admin');Siempre pon await a las operaciones async en los tests.
Cuándo NO usar try/catch en los tests
No uses try/catch para ocultar fallos de aserciones. Si una aserción falla, el test debe fallar:
// Mal: silencia el fallo de la aserción, el test "pasa" cuando no debería
try {
await expect(page.locator('[data-testid="success"]')).toBeVisible();
} catch (error) {
console.log('El elemento no es visible pero continuamos...');
}
// Bien: dejá que los fallos de aserciones fallen el test
await expect(page.locator('[data-testid="success"]')).toBeVisible();try/catch en los tests sirve para código de setup y teardown donde el fallo no debería enmascarar el test real, resiliencia intencional (lógica de reintentos, interacciones opcionales) y creación de datos de prueba donde quieres un mensaje de error claro y personalizado.
No sirve para silenciar aserciones.
Resumen rápido
try/catchmaneja errores: el código entrycorre,catchmaneja cualquier error lanzadofinallysiempre corre: úsalo para limpieza- Siempre pon
awaita las operaciones async: los fallos async sin await son difíciles de depurar throwintencionalmente cuando quieres un mensaje de error específico- No captures fallos de aserciones: deben fallar el test
instanceofpermite verificar el tipo de error y manejar distintos errores de forma diferente