Handle failures with try/catch, throw clear errors, and plan for empty, missing and invalid input.
Why this matters
Code that only works when everything goes right is a demo, not software. Real users type letters into number fields, lose their connection halfway through a request, and paste text you never imagined. Real data arrives empty, incomplete or malformed. Professional code expects this: it validates input, handles failures without crashing, and gives the user a clear way forward. This lesson brings together everything in the course with that mindset.
try, catch, finally
When code might throw an error, wrap it in try. If anything inside throws, control jumps to catch, and the error object tells you what happened:
try {
const settings = JSON.parse('{"theme": dark}'); // invalid JSON
console.log(settings.theme);
} catch (err) {
console.error("Could not read settings:", err.message);
} finally {
console.log("This runs either way");
}
catch (err)receives the error;err.nameis the type (SyntaxError) anderr.messagethe description.finallyruns whether or not there was an error, which is handy for cleanup such as hiding a spinner.- Code after the whole block keeps running, so one bad value does not stop the page.
Fallback values
A very common pattern is "try it, and use a sensible default if it fails":
function safeParseJSON(text, fallback) {
try {
return JSON.parse(text);
} catch {
return fallback;
}
}
safeParseJSON('{"a":1}', {}); // { a: 1 }
safeParseJSON("not json", {}); // {}
(catch without (err) is allowed when you do not need the error.)
Throwing your own errors
When a function receives input it cannot work with, the clearest response is often to throw an error with a helpful message, instead of returning a confusing value like NaN:
function parseAge(text) {
const age = Number(text);
if (text.trim() === "" || !Number.isInteger(age)) {
throw new Error(`"${text}" is not a whole number`);
}
if (age < 0 || age > 150) {
throw new RangeError(`Age ${age} is out of range`);
}
return age;
}
The caller decides what to do: show the message next to the form field, ask again, or log it. Throwing an Error object (not a string) gives you a stack trace showing where it came from. Built-in types like TypeError and RangeError say what kind of problem it is.
Note the empty-string check: Number("") is 0, not NaN, a classic edge case.
Errors in async code
With async/await, a rejected promise throws at the await, so the same try/catch works:
async function loadWithFallback(fetchFn, fallback) {
try {
const res = await fetchFn();
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (err) {
console.warn("Using fallback data:", err.message);
return fallback;
}
}
Note the return await inside try. Without await, a rejection would escape the catch and reach the caller instead.
If you use .then chains, add a .catch at the end. An unhandled rejection does not crash the page, but whatever was supposed to happen next simply never happens, which is confusing for users.
Thinking in edge cases
Before you call a function finished, ask what happens with unusual input. A checklist for most functions:
| input | ask |
|---|---|
| empty | "", [], {}: is there a sensible answer (0, null, a message)? |
| missing | undefined or null arguments or properties |
| wrong type | "5" instead of 5; an object instead of an array |
| boundary | 0, negative numbers, the first and last item, very large values |
| failure | the network is down, the server returns 500, the JSON is broken |
For example, an average function:
function average(nums) {
if (!Array.isArray(nums) || nums.length === 0) return 0;
return nums.reduce((sum, n) => sum + n, 0) / nums.length;
}
Without the guard, average([]) is 0 / 0, which is NaN, and NaN will then appear on screen as "NaN".
Optional chaining and nullish coalescing help with missing data:
const city = user?.address?.city ?? "Unknown";
?. stops and gives undefined instead of throwing when something is null or undefined; ?? supplies a default only for null/undefined (unlike ||, which also replaces 0 and "").
When not to catch
Do not wrap everything in try/catch and ignore the error. A silent catch {} hides real bugs and makes debugging much harder. Catch where you can do something useful (show a message, use a fallback, retry), and let unexpected errors surface so you find them.
Common mistakes
- Empty catch blocks that swallow errors with no message or fallback.
- Throwing strings instead of
Errorobjects (no stack trace). - Forgetting
return awaitinsidetryin an async function. - Trusting
Number()without checking forNaNand empty strings. - Showing raw error messages to users. Log the details; show a plain, helpful sentence.
Quick recap
try/catch/finallyhandle errors; fallbacks keep the page working.- Throw
Errorobjects with clear messages for input you cannot handle. - Async errors are caught at
await; add.catchto promise chains. - Check empty, missing, wrong-type, boundary and failure cases;
?.and??help with missing data.
Try it
Open the Do tab and make three functions robust: a JSON parser with a fallback, an age validator that throws, and an async loader that falls back when the request fails.