Error handling
try/catch/finally, throwing properly, custom errors and when not to catch anything.
Updated
What an error is
An error (exception) is a signal that something went wrong and the program can't continue normally at that point. When it happens, JS stops the current function and "climbs" up the calls until it finds someone who catches it. If nobody does → an error in the console, and in React → the error screen.
An error is an object with:
name— the type (TypeError,HttpError);message— what happened;stack— where it happened (the chain of calls) — the most useful piece of information when debugging;cause(optional) — the original error that caused it.
Native error types
| Type | When it happens | Example |
|---|---|---|
TypeError |
an operation on the wrong type | undefined.name, 5() |
ReferenceError |
a variable that doesn't exist / is in the TDZ | using an undeclared x |
SyntaxError |
invalid code or invalid JSON | JSON.parse('{nope') |
RangeError |
a value out of bounds | new Array(-1) |
Expected vs unexpected errors
The most important distinction:
| Expected | Unexpected | |
|---|---|---|
| Examples | an invalid email, an out-of-stock product, a 404 | a bug, a server down, undefined.x |
| How you handle them | returned values: { ok: false, error } |
exceptions: throw |
| Who sees them | the user, with a clear message | the logs / monitoring; the user sees a generic message |
Validating a form isn't "exceptional" — it's normal for the user to make mistakes. Return a result, don't throw.
try / catch / finally
try {
const data = JSON.parse(raw) // code that may throw
render(data)
} catch (error) {
showToast('Invalid data') // runs only if an error happened
} finally {
setLoading(false) // always runs, with or without an error
}throw — how to throw properly
throw new Error('User not found') // ✓ has a stack trace
throw new Error('Request failed', { cause: originalError }) // ✓ keeps the cause
throw 'something' // ✗ no stack, no typeCustom errors
When the code that catches has to tell error types apart:
class HttpError extends Error {
constructor(status, message) {
super(message)
this.name = 'HttpError'
this.status = status
}
}
try {
await api('/users/5')
} catch (e) {
if (e instanceof HttpError && e.status === 404) return notFound()
throw e // what you can't handle, you throw further up
}Errors in async code
async function load() {
try {
const res = await fetch('/api')
if (!res.ok) throw new HttpError(res.status, 'Request failed')
return await res.json()
} catch (e) {
console.error(e)
return null
}
}A rejected Promise that nobody catches = unhandledrejection. Details in Async.
Where to catch errors — the rule
- Catch where you can do something useful: a message for the user, a retry, a fallback value.
- Don't catch "so it doesn't crash" — a swallowed error hides the bug.
- Never an empty
catch {}. - In React / Next:
error.tsxis the safety net for whatever slips through.
Summary
- An error = an object with
name,message,stack; it climbs until it's caught. - Expected errors → returned values; unexpected ones →
throw new Error(...). try/catch/finally; custom errors withextends Error; don't swallow errors.