Practise JavaScript exception handling with 30 programs, each with a full solution, the exact output and a short explanation. The programs start with the basics, move to intermediate problems and finish with real-world scenarios from test automation, such as test data, prices, form inputs and test results. They are written for testers learning JavaScript for Playwright.
How to practise: read the task, write your own solution first, then open Show solution to compare. Run any program with node program.js.
New to this topic? Read the JavaScript Exception Handling lesson first.
- Beginner programs (1–10)
- Intermediate programs (11–19)
- Real-world QA scenario programs (20–30)
JavaScript Beginner Programs
1. Basic try/catchBeginner
Parse invalid JSON inside try/catch and print the error name.
Show solution
try {
JSON.parse('{ invalid json }');
} catch (error) {
console.log('Caught:', error.name);
}
console.log('Program continues');
Output
Caught: SyntaxError
Program continues
Explanation: catch receives the thrown error, so the program can continue instead of crashing.
2. finally always runsBeginner
Show that finally runs both when an error happens and when it does not.
Show solution
for (const input of ['{"ok":true}', 'oops']) {
try {
JSON.parse(input);
console.log('Parsed', input);
} catch (error) {
console.log('Failed to parse', input);
} finally {
console.log('Cleanup done');
}
}
Output
Parsed {"ok":true}
Cleanup done
Failed to parse oops
Cleanup done
Explanation: finally is the place for cleanup, such as closing files or browsers, because it always runs.
3. Throw a custom errorBeginner
Write divide(a, b) that throws an error when b is 0.
Show solution
function divide(a, b) {
if (b === 0) throw new Error('Cannot divide by zero');
return a / b;
}
try {
console.log(divide(10, 2));
console.log(divide(5, 0));
} catch (error) {
console.log('Error:', error.message);
}
Output
5
Error: Cannot divide by zero
Explanation: throw stops the function and passes the error to the nearest catch.
4. Error propertiesBeginner
Print the name and message of a caught error.
Show solution
try {
throw new Error('Login button not found');
} catch (error) {
console.log(error.name);
console.log(error.message);
console.log(error instanceof Error);
}
Output
Error
Login button not found
true
Explanation: every Error has a name, a message and a stack trace.
5. ReferenceErrorBeginner
Use an undefined variable inside try and print the error type.
Show solution
try {
console.log(undefinedVariable);
} catch (error) {
console.log(error.name);
}
Output
ReferenceError
Explanation: using a name that was never declared throws a ReferenceError.
6. TypeErrorBeginner
Call a method on undefined and print the error type.
Show solution
const response = {};
try {
response.data.items.length;
} catch (error) {
console.log(error.name);
}
Output
TypeError
Explanation: reading a property of undefined throws a TypeError, one of the most common test-script errors.
7. RangeErrorBeginner
Create an array with a negative length and catch the error.
Show solution
try {
new Array(-1);
} catch (error) {
console.log(error.name, '-', error.message);
}
Output
RangeError - Invalid array length
Explanation: a RangeError means a value is outside the allowed range.
8. Validate input and throwBeginner
Throw a TypeError when age is not a number.
Show solution
function setAge(age) {
if (typeof age !== 'number') throw new TypeError('age must be a number');
return `Age set to ${age}`;
}
for (const value of [30, 'thirty']) {
try {
console.log(setAge(value));
} catch (error) {
console.log(`${error.name}: ${error.message}`);
}
}
Output
Age set to 30
TypeError: age must be a number
Explanation: throwing the most specific built-in error type tells callers what went wrong.
9. catch without a bindingBeginner
Use catch without naming the error variable when you do not need it.
Show solution
function isValidJson(text) {
try {
JSON.parse(text);
return true;
} catch {
return false;
}
}
console.log(isValidJson('[1, 2]'));
console.log(isValidJson('[1, 2'));
Output
true
false
Explanation: modern JavaScript allows catch { } without (error) when the error itself is not used.
10. Return a default on failureBeginner
Read a number from text and fall back to 0 when it is invalid.
Show solution
function toNumberOrZero(text) {
try {
const n = Number(text);
if (Number.isNaN(n)) throw new Error('not a number');
return n;
} catch {
return 0;
}
}
console.log(toNumberOrZero('42'), toNumberOrZero('abc'));
Output
42 0
Explanation: catching and returning a default keeps the program running with a safe value.
JavaScript Intermediate Programs
11. Custom error classIntermediate
Create ValidationError with a field property and catch it.
Show solution
class ValidationError extends Error {
constructor(field, message) {
super(message);
this.name = 'ValidationError';
this.field = field;
}
}
try {
throw new ValidationError('email', 'Email is required');
} catch (error) {
console.log(error.name, error.field, error.message);
}
Output
ValidationError email Email is required
Explanation: custom error classes carry extra details and can be identified by type.
12. Handle different error typesIntermediate
Handle ValidationError and other errors differently with instanceof.
Show solution
class ValidationError extends Error {}
function run(action) {
try {
action();
} catch (error) {
if (error instanceof ValidationError) console.log('Fix your input:', error.message);
else if (error instanceof TypeError) console.log('Bug in code:', error.message);
else console.log('Unknown error:', error.message);
}
}
run(() => { throw new ValidationError('password too short'); });
run(() => { null.click(); });
run(() => { throw new Error('network down'); });
Output
Fix your input: password too short
Bug in code: Cannot read properties of null (reading 'click')
Unknown error: network down
Explanation: instanceof lets one catch block react differently to each kind of error.
13. Rethrow unexpected errorsIntermediate
Handle only SyntaxError while parsing and rethrow anything else.
Show solution
function parseConfig(text) {
try {
return JSON.parse(text);
} catch (error) {
if (error instanceof SyntaxError) return {};
throw error;
}
}
console.log(parseConfig('{"retries":2}'));
console.log(parseConfig('not json'));
Output
{ retries: 2 }
{}
Explanation: catch only what you can handle. Rethrowing other errors keeps real bugs visible.
14. Error cause chainingIntermediate
Wrap a low-level error in a higher-level one using cause.
Show solution
function loadUser() {
try {
JSON.parse('{bad');
} catch (error) {
throw new Error('Could not load user', { cause: error });
}
}
try {
loadUser();
} catch (error) {
console.log(error.message);
console.log('Caused by:', error.cause.name);
}
Output
Could not load user
Caused by: SyntaxError
Explanation: cause preserves the original error, so you get a clear message without losing the root cause.
15. Nested try/catchIntermediate
Use an inner try/catch for an optional step and an outer one for critical steps.
Show solution
try {
console.log('Step 1: open page');
try {
throw new Error('cookie banner not found');
} catch (inner) {
console.log('Optional step skipped:', inner.message);
}
console.log('Step 2: login');
throw new Error('login failed');
} catch (outer) {
console.log('Test failed:', outer.message);
}
Output
Step 1: open page
Optional step skipped: cookie banner not found
Step 2: login
Test failed: login failed
Explanation: the inner catch handles a recoverable problem, while the outer one still catches fatal errors.
16. finally with returnIntermediate
Show that finally runs even when try returns a value.
Show solution
function getStatus() {
try {
return 'from try';
} finally {
console.log('finally runs before the value is returned');
}
}
console.log(getStatus());
Output
finally runs before the value is returned
from try
Explanation: finally runs before the function actually returns, which guarantees cleanup.
17. Collect errors instead of stoppingIntermediate
Validate several fields and collect all errors.
Show solution
const checks = [
() => { if (!'qa@test.com'.includes('@')) throw new Error('bad email'); },
() => { if ('123'.length < 8) throw new Error('password too short'); },
() => { if (Number('abc') !== Number('abc')) throw new Error('age is not a number'); }
];
const errors = [];
for (const check of checks) {
try { check(); } catch (error) { errors.push(error.message); }
}
console.log(errors.join(' | '));
Output
password too short | age is not a number
Explanation: catching inside the loop lets every check run, like Playwright’s soft assertions.
18. Error-first result objectsIntermediate
Return { ok, value, error } instead of throwing.
Show solution
function safeParse(text) {
try {
return { ok: true, value: JSON.parse(text) };
} catch (error) {
return { ok: false, error: error.message.split(' in JSON')[0] };
}
}
console.log(safeParse('[1]').ok);
console.log(safeParse('{oops').ok);
Output
true
false
Explanation: result objects make failures part of the normal return value, which is handy in data pipelines.
19. Assertion functionIntermediate
Write assert(condition, message) that throws when the condition is false.
Show solution
function assert(condition, message) {
if (!condition) throw new Error(`Assertion failed: ${message}`);
}
try {
assert(2 + 2 === 4, 'maths works');
console.log('First assertion passed');
assert('Login'.length === 4, 'title length');
} catch (error) {
console.log(error.message);
}
Output
First assertion passed
Assertion failed: title length
Explanation: tests are built on assertions that throw when expectations are not met.
JavaScript Real-World QA Scenario Programs
20. Retry on failureReal-world
Retry a flaky action up to 3 times before giving up.
Show solution
let calls = 0;
function clickButton() {
calls++;
if (calls < 3) throw new Error(`element detached (call ${calls})`);
return 'clicked';
}
let result;
for (let attempt = 1; attempt <= 3; attempt++) {
try {
result = clickButton();
break;
} catch (error) {
console.log(`Attempt ${attempt} failed: ${error.message}`);
}
}
console.log('Result:', result);
Output
Attempt 1 failed: element detached (call 1)
Attempt 2 failed: element detached (call 2)
Result: clicked
Explanation: catch, log and retry. break leaves the loop as soon as an attempt succeeds.
21. Safe JSON test-data loaderReal-world
Load JSON test data and report a clear message for invalid content.
Show solution
function loadTestData(name, text) {
try {
const data = JSON.parse(text);
if (!Array.isArray(data)) throw new Error('expected an array');
return data;
} catch (error) {
console.log(`${name}: ${error instanceof SyntaxError ? 'invalid JSON' : error.message}`);
return [];
}
}
console.log(loadTestData('users.json', '[{"id":1}]').length);
console.log(loadTestData('broken.json', '[{"id":1}').length);
console.log(loadTestData('object.json', '{"id":1}').length);
Output
1
broken.json: invalid JSON
0
object.json: expected an array
0
Explanation: clear error messages save time when test data files are malformed.
22. Validate a signup formReal-world
Throw a ValidationError for the first invalid field and show it to the user.
Show solution
class ValidationError extends Error {
constructor(field, message) { super(message); this.name = 'ValidationError'; this.field = field; }
}
function validate({ email, password }) {
if (!email?.includes('@')) throw new ValidationError('email', 'Enter a valid email');
if (!password || password.length < 8) throw new ValidationError('password', 'Use at least 8 characters');
return 'Form is valid';
}
for (const form of [{ email: 'qa', password: '12345678' }, { email: 'qa@test.com', password: '123' }, { email: 'qa@test.com', password: 'Secret123' }]) {
try { console.log(validate(form)); }
catch (e) { console.log(`[${e.field}] ${e.message}`); }
}
Output
[email] Enter a valid email
[password] Use at least 8 characters
Form is valid
Explanation: a field name on the error lets the UI show the message next to the right input.
23. Map HTTP status codes to errorsReal-world
Throw a specific error for each failing status code.
Show solution
class HttpError extends Error {
constructor(status) { super(`HTTP ${status}`); this.status = status; }
}
function checkResponse(status) {
if (status >= 200 && status < 300) return 'OK';
throw new HttpError(status);
}
for (const status of [200, 404, 503]) {
try {
console.log(status, checkResponse(status));
} catch (error) {
const hint = error.status >= 500 ? 'server problem, retry later' : 'check the request';
console.log(status, error.message, '-', hint);
}
}
Output
200 OK
404 HTTP 404 - check the request
503 HTTP 503 - server problem, retry later
Explanation: a status code on the error lets callers decide whether to retry or fail fast.
24. Always release resourcesReal-world
Simulate opening and closing a browser, closing it even when the test fails.
Show solution
function runTest(shouldFail) {
console.log('Browser opened');
try {
if (shouldFail) throw new Error('assertion failed');
console.log('Test passed');
} catch (error) {
console.log('Test failed:', error.message);
} finally {
console.log('Browser closed');
}
}
runTest(false);
runTest(true);
Output
Browser opened
Test passed
Browser closed
Browser opened
Test failed: assertion failed
Browser closed
Explanation: finally guarantees cleanup, which is what Playwright fixtures do after use().
25. Soft assertionsReal-world
Check several page values and report all failures at the end.
Show solution
const page = { title: 'Shop', cartCount: 2, user: 'guest' };
const expected = { title: 'Shop', cartCount: 3, user: 'asha' };
const failures = [];
for (const key of Object.keys(expected)) {
try {
if (page[key] !== expected[key]) throw new Error(`${key}: expected ${expected[key]}, got ${page[key]}`);
} catch (error) {
failures.push(error.message);
}
}
console.log(failures.length ? `${failures.length} failures\n- ${failures.join('\n- ')}` : 'All checks passed');
Output
2 failures
- cartCount: expected 3, got 2
- user: expected asha, got guest
Explanation: collecting failures shows every problem in one run, like expect.soft in Playwright.
26. Timeout errorReal-world
Throw a TimeoutError when a simulated wait exceeds its limit.
Show solution
class TimeoutError extends Error {
constructor(ms) { super(`Timed out after ${ms} ms`); this.name = 'TimeoutError'; }
}
function waitForElement(appearsAfterMs, timeoutMs) {
if (appearsAfterMs > timeoutMs) throw new TimeoutError(timeoutMs);
return `Element visible after ${appearsAfterMs} ms`;
}
for (const appears of [800, 7000]) {
try { console.log(waitForElement(appears, 5000)); }
catch (e) { console.log(`${e.name}: ${e.message}`); }
}
Output
Element visible after 800 ms
TimeoutError: Timed out after 5000 ms
Explanation: a named error type makes timeouts easy to recognise in reports and retry logic.
27. Error message for a missing elementReal-world
Build a helpful error message that lists the available options when a menu item is missing.
Show solution
function clickMenuItem(menu, name) {
if (!menu.includes(name)) {
throw new Error(`Menu item '${name}' not found. Available: ${menu.join(', ')}`);
}
return `Clicked ${name}`;
}
const menu = ['Home', 'Shop', 'Contact'];
try {
console.log(clickMenuItem(menu, 'Shop'));
console.log(clickMenuItem(menu, 'Blog'));
} catch (error) {
console.log(error.message);
}
Output
Clicked Shop
Menu item 'Blog' not found. Available: Home, Shop, Contact
Explanation: including context in error messages turns a vague failure into an obvious fix.
28. Fail fast on missing configReal-world
Throw at start-up when required environment variables are missing.
Show solution
function requireEnv(env, names) {
const missing = names.filter(name => !env[name]);
if (missing.length) throw new Error(`Missing environment variables: ${missing.join(', ')}`);
return 'Config OK';
}
try {
console.log(requireEnv({ BASE_URL: 'https://qa.app.test', USERNAME: 'qa' }, ['BASE_URL', 'USERNAME']));
console.log(requireEnv({ BASE_URL: 'https://qa.app.test' }, ['BASE_URL', 'USERNAME', 'PASSWORD']));
} catch (error) {
console.log(error.message);
}
Output
Config OK
Missing environment variables: USERNAME, PASSWORD
Explanation: failing immediately with a clear message beats confusing failures halfway through a test run.
29. Log errors with contextReal-world
Wrap a step so errors include the step name.
Show solution
function step(name, fn) {
try {
fn();
console.log(`✔ ${name}`);
} catch (error) {
throw new Error(`Step "${name}" failed: ${error.message}`);
}
}
try {
step('open home page', () => {});
step('add item to cart', () => { throw new Error('button disabled'); });
step('checkout', () => {});
} catch (error) {
console.log(error.message);
}
Output
✔ open home page
Step "add item to cart" failed: button disabled
Explanation: adding the step name to the error is what Playwright’s test.step does in reports.
30. Count error types in a logReal-world
Count how many errors of each type appear in a list of caught errors.
Show solution
const caught = [new TypeError('a'), new Error('b'), new TypeError('c'), new RangeError('d')];
const counts = {};
for (const e of caught) counts[e.name] = (counts[e.name] || 0) + 1;
console.log(counts);
Output
{ TypeError: 2, Error: 1, RangeError: 1 }
Explanation: grouping by error.name quickly shows which kind of failure dominates a test run.
2 thoughts on “JavaScript Exception Handling Practice Programs with Solutions (30 Programs)”