Error Handling in JavaScript: try/catch
SkillVeris Team
Engineering Team

try/catch runs code that might fail and, if an error is thrown, jumps to the catch block instead of crashing the program.
In this guide, you'll learn:
- The catch block receives an error object with a name, message, and stack you can inspect and log.
- A finally block runs whether or not an error occurred, making it ideal for cleanup like closing resources.
- You throw your own errors with the throw keyword, ideally using Error or a custom subclass.
- Async errors need try/catch inside an async function or a .catch on the promise — a plain try/catch will not catch a rejected promise you did not await.
1What Is try/catch?
try/catch is JavaScript's mechanism for handling errors gracefully. You put code that might fail inside a try block; if it throws an error, execution jumps immediately to the catch block instead of crashing the whole program. This lets you recover, show a helpful message, or retry rather than letting one failure take everything down.
Errors are a normal part of real software — a network request times out, a file is missing, user input is malformed. try/catch gives you a structured way to deal with these events. Combined with throwing your own errors and a finally block for cleanup, it forms the core of reliable JavaScript.
2The Basic Syntax
The structure is simple: a try block with the risky code, a catch block that receives the error, and an optional finally block that always runs. The error object passed to catch carries useful details you should inspect and usually log.
- try {
- const data = JSON.parse(input) # may throw on bad input
- } catch (error) {
- console.error(error.message) # handle it
- } finally {
- cleanup() # always runs
- }
💡Pro Tip
The error object has name, message, and stack properties. Log error.stack during development to see exactly where the failure originated.
3Throwing Your Own Errors
You do not have to wait for the runtime to throw. The throw keyword lets you signal a problem yourself, which is essential for validating input and enforcing rules. Always throw an Error object rather than a plain string, because Error carries a stack trace and works properly with catch.
- if (age < 0) throw new Error('Age cannot be negative')
- throw new TypeError('Expected a number') # a built-in error subclass
- throw 'bad' # avoid this — strings lose the stack trace
Custom Error Classes
For larger apps, subclass Error to create meaningful types like ValidationError or NotFoundError. Your catch blocks can then check the error type and respond differently to each.
class ValidationError extends Error { }
throw new ValidationError('Email is required')4The finally Block
finally runs no matter what — whether the try succeeded, threw an error, or even returned early. That guarantee makes it the right place for cleanup that must happen regardless of outcome, such as closing a file handle, releasing a lock, or hiding a loading spinner.
Because finally always executes, you avoid duplicating cleanup code in both the success and failure paths. Put the work in try, the recovery in catch, and the guaranteed cleanup in finally, and each concern stays in its own place.
5Handling Errors in Async Code
Asynchronous code needs extra care. A plain try/catch around a function that returns a promise will not catch a rejection unless you await it. With async/await, wrap the awaited call in try/catch and it works as expected. With raw promises, attach a .catch to the chain instead.
The most common async bug is an unhandled promise rejection — a failed promise nobody caught. It can crash a Node process or silently swallow errors in the browser. Awaiting inside try/catch, or always chaining .catch, keeps async failures under control.
- async function load() {
- try { const res = await fetch(url); return await res.json() }
- catch (err) { console.error('Load failed', err) }
- }
- fetch(url).then(handle).catch(err => console.error(err)) # promise style
⚠️Watch Out
A try/catch that wraps a promise without awaiting it will not catch the rejection. Await the promise inside try, or attach a .catch to the chain.
6Rethrowing and Global Handlers
Sometimes a catch block is not the right place to fully resolve an error. You can inspect it, log some context, and then rethrow so a higher layer can decide what to do. This keeps low-level code from silently swallowing problems it cannot actually fix.
At the very top, catch what nothing else did. Browsers fire a window error and unhandledrejection event, and Node exposes process-level handlers for uncaught exceptions and unhandled rejections. Wire these to your logging so no failure disappears without a trace.
- catch (err) { logContext(err); throw err } # handle partially, then rethrow
- window.addEventListener('unhandledrejection', e => report(e.reason))
- process.on('uncaughtException', err => { report(err); process.exit(1) })
7Best Practices for Error Handling
Effective error handling is about catching what you can meaningfully act on and letting the rest surface loudly. These habits keep bugs visible instead of hidden.
- Catch only what you can handle; do not wrap everything in an empty catch that swallows errors.
- Always throw Error objects, never plain strings, so stack traces survive.
- Log enough context — the operation, inputs, and error — to diagnose the failure later.
- Use finally for cleanup so resources are released on every path.
- Show users a friendly message while logging the technical detail for developers.
8Key Takeaways
Reliable JavaScript rests on a handful of error-handling principles.
- try/catch runs risky code and diverts to catch on failure instead of crashing.
- The catch block receives an error object with name, message, and stack.
- finally always runs, making it ideal for cleanup.
- Throw Error objects, not strings, and consider custom error classes for larger apps.
- For async code, await inside try/catch or attach a .catch to the promise chain.
9Frequently Asked Questions
Q: When does the finally block run? A: finally runs after try and catch regardless of the outcome — success, a caught error, or even an early return. It is the reliable place for cleanup that must happen no matter what.
Q: Why should I throw Error objects instead of strings? A: Error objects carry a stack trace and standard properties like name and message, and they interoperate correctly with tooling and catch blocks. A thrown string loses the stack trace and makes debugging harder.
Q: Why is my try/catch not catching an async error? A: A plain try/catch only catches errors thrown synchronously or from an awaited promise. If you call an async function without awaiting it inside the try, the rejection escapes. Await the call, or attach a .catch to the promise.
Q: Should I wrap all my code in try/catch? A: No. Catch only where you can meaningfully respond or recover. Blanket try/catch that silently swallows errors hides bugs. Let unexpected errors propagate to a top-level handler that logs them.
Related Reading
Get The Print Version
Download a PDF of this article for offline reading.
About the Publisher
SkillVeris Team
Engineering Team
Our engineering writers turn abstract code concepts into hands-on, project-driven learning experiences.
View all postsRelated Posts
Never miss an update
Get the latest tutorials and guides delivered to your inbox.
No spam. Unsubscribe anytime.