try / catch
Catching errors before they crash your test — and deciding what to do.
Step 1
The idea
When an asynchronous operation fails — network error, element not found, timeout — it throws an error. If nothing catches that error, your entire test crashes with an ugly stack trace.
try/catch lets you handle errors gracefully: "try to do this — if it fails, catch the error and do something useful instead."
Think of it like a safety net under a trapeze act. The performer tries the move. If they fall, the net catches them — they don't hit the floor.
Step 2
See it run
Attempting to load page...
Page loaded successfully
---
Attempting to load page...
Failed to load: Network timeout after 5000ms
Retrying with a backup URL...
Backup loaded successfully// Simulated async functions
function loadPage(url) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (url.includes("bad")) {
reject(new Error("Network timeout after 5000ms"));
} else {
resolve(`Page loaded: ${url}`);
}
}, 100);
});
}
// Without try/catch — an error here crashes the whole script
// await loadPage("bad-url"); // ← would crash
// WITH try/catch — errors are handled gracefully
async function runTest(url) {
console.log("Attempting to load page...");
try {
const result = await loadPage(url);
console.log("Page loaded successfully");
} catch (error) {
console.log("Failed to load:", error.message);
console.log("Retrying with a backup URL...");
const backup = await loadPage("https://backup.example.com");
console.log("Backup loaded successfully");
}
}
runTest("https://good-url.com");
setTimeout(() => {
console.log("---");
runTest("https://bad-url.com");
}, 200);$ node try-catch.jsStep 3
Now you try
Add a finally block to the example above:
try {
// your code
} catch (error) {
// handle error
} finally {
console.log("Test run complete — regardless of success or failure");
}finally always runs — whether try succeeded or catch ran. It's perfect for cleanup: closing browsers, resetting state, writing logs.
Step 4
Why a tester cares
try/catch is used whenever you're reading user input or external data that might not be in the right format. Here's a JSON parser with proper error handling:
async function loadConfig(jsonString) {
try {
const config = JSON.parse(jsonString);
// Validate required fields exist
if (!config.apiUrl) throw new Error("Missing required field: apiUrl");
if (!config.timeout) throw new Error("Missing required field: timeout");
console.log("Config loaded successfully");
console.log(` API: ${config.apiUrl}`);
console.log(` Timeout: ${config.timeout}ms`);
return config;
} catch (error) {
console.error(`Config error: ${error.message}`);
return null; // return null instead of crashing
} finally {
console.log("--- load attempt complete ---");
}
}
// Test with valid JSON
await loadConfig('{"apiUrl":"https://api.example.com","timeout":5000}');
// Test with invalid JSON
await loadConfig('this is not json');
// Test with missing field
await loadConfig('{"apiUrl":"https://api.example.com"}');$ node parse-input.jsThe finally block always runs — perfect for cleanup or logging. The function never crashes: it either returns the config or returns null. That's graceful error handling.
Recap
- 1try { } contains code that might fail. catch (error) { } handles the failure.
- 2finally { } always runs — useful for cleanup regardless of outcome.
- 3Re-throw the error (throw error) if you want to handle it AND still mark the test as failed.