JavaScript try…catch Error Handling
Learn JavaScript error handling with try, catch, finally, and throw. Covers the Error object, built-in error types, and rethrowing.
Mastering Error Handling in JavaScript with Try...Catch
Handling errors effectively is crucial for building robust applications in JavaScript. When something goes wrong at runtime — a network call fails, JSON is malformed, a variable is undefined — JavaScript throws an exception. Without handling, that exception stops your script. This article covers the try...catch statement, the finally clause, the throw operator, the Error object and its built-in subtypes, rethrowing, and the important limitation that synchronous try...catch cannot catch errors thrown inside asynchronous callbacks.
Understanding Try...Catch in JavaScript
The try...catch statement is a tool for managing exceptions — errors that occur during the execution of the program. It lets you handle these exceptions gracefully instead of letting them crash the whole script.
The mechanism has two blocks:
try— the code that might throw. JavaScript runs it normally.catch (error)— runs only if something in thetryblock throws. The thrown value is passed in aserror.
If no error occurs, the catch block is skipped entirely. If an error occurs, execution jumps to catch immediately — the rest of the try block is not executed.
Basic Syntax of Try...Catch
Here's a simple example to demonstrate the basic structure of try...catch:
In this example, any error that occurs within the try block is caught by the catch block, where it can be handled without causing the script to crash.
Throwing Your Own Errors with throw
The throw operator generates an exception. You can throw any value, but the convention — and the only thing you should do in practice — is to throw an Error object (or an instance of one of its subtypes). Doing so gives you a useful message and a stack trace automatically.
Avoid throw 'a string' or throw 42. A thrown string has no message, no name, and no stack, so callers that expect an Error object (as most code does) will behave incorrectly. For domain-specific errors with extra fields, define your own class — see custom errors, extending Error.
The Error Object
When you do new Error(message), you get an object with three commonly used properties:
name— the type of error, e.g."Error","TypeError","SyntaxError".message— the human-readable description you passed to the constructor.stack— a string with the call stack at the moment the error was created (non-standard but supported by every major engine; great for debugging, not for control flow).
Built-in Error Types
JavaScript ships several Error subclasses that the engine throws automatically. Knowing them helps you write precise catch logic:
| Type | Thrown when |
|---|---|
SyntaxError | Code or data is malformed, e.g. JSON.parse() on invalid JSON. |
TypeError | A value is not of the expected type, e.g. calling a non-function or reading a property of undefined. |
ReferenceError | A variable that doesn't exist is referenced. |
RangeError | A value is outside its allowed range, e.g. an invalid array length. |
URIError | decodeURIComponent() or similar receives a malformed URI. |
EvalError | Historical; rarely thrown by modern engines. |
Each of these has name set to its type, so you can branch on instanceof:
Handling Specific Errors
You can also handle specific types of errors by examining the error object:
This example specifically handles SyntaxError that may occur during JSON parsing. If the error caught is an instance of SyntaxError, it is handled by logging a specific message. If it is not, the error is rethrown, potentially to be caught by a higher-level error handler or to crash the program, indicating an unhandled error scenario.
Rethrowing Errors
A catch block catches every error in its try, including ones you don't know how to handle. The recommended pattern is: inspect the error, handle the cases you understand, and rethrow everything else so it propagates to an outer handler. Swallowing unknown errors silently hides real bugs.
Optional Catch Binding
If you don't need the error value, you can omit the binding entirely (ES2019+). This is useful when the mere fact that something failed is all you care about:
Using Finally
The finally clause executes after the try and catch blocks, regardless of whether an exception was thrown or caught. It is useful for cleaning up resources or performing cleanup tasks, irrespective of the outcome of the try...catch:
This ensures that the "Finally block executed" message is logged whether an error occurs or not, demonstrating how finally can be used to perform necessary cleanup actions.
Real API Request Examples
Using the JSONPlaceholder API is a fantastic way to practice handling real-world data in JavaScript, especially when working with asynchronous requests and handling potential errors that might arise during these operations. Here are a couple of real-world examples using the JSONPlaceholder API, which offers fake online REST data that you can experiment with for testing and prototyping.
Example 1: Fetching Posts and Handling Errors
In this example, we fetch posts from the JSONPlaceholder API using fetch and handle potential network errors or issues with the API response:
This script makes an HTTP request to retrieve a single todo item. It checks if the response is successful (i.e., HTTP status 200-299). If not, it throws an error with the response status. Any errors, either from network issues or from the throw statement, are caught in the catch block and logged. The finally block executes regardless of the result, ensuring any necessary cleanup or final operations are performed.
Example 2: Posting Data and Handling Exceptions
Here, we demonstrate how to send data to the server using POST method and handle exceptions appropriately:
In this script, we are sending a new post to the server. The fetch function is used with the POST method, including headers and a JSON stringified body. If the server response indicates a failure (non-2xx HTTP status), an error is thrown, which is then caught and handled in the catch block. Regardless of success or failure, the finally block ensures that the operation is marked as complete.
Example 3: Deliberately Causing and Handling an Error
This example intentionally requests a user ID that does not exist on the JSONPlaceholder API, triggering a 404 Not Found error, which we will catch and handle.
How This Example Works
- Invalid API Endpoint: The
fetchfunction is called with a URL that includes an invalid user ID (99999). Given that JSONPlaceholder typically doesn't have a user at this index, the API will return a 404 error. - Check Response Validity: The code checks if the response status is not in the successful range (200-299). Given that the user ID is invalid, the API response will likely be 404, triggering our error handling in the
if (!response.ok)check. - Error Throwing: Since the response is not OK, an error is thrown with the message including the HTTP status, which in this case will indicate a 404 Not Found error.
- Catch Block: The catch block captures the thrown error and logs a specific message using
console.log. This provides clear feedback about what went wrong. - Finally Block: This block is used for cleanup or final statements, indicating the completion of the attempt, regardless of the outcome.
Why Try...Catch Can't Catch Async Callback Errors
This is the single most common gotcha with try...catch. The statement is synchronous: it only catches errors thrown while the try block is currently running. When you schedule a callback with setTimeout, an event listener, or an unawaited promise, the callback runs later — after the try...catch has already finished and the call stack is empty. By then there is no surrounding try to catch anything.
To handle the error, the try...catch must live inside the callback, where the error is actually thrown:
The same rule explains why try...catch works with async/await but not with bare promises. await pauses the function and resumes it in the same logical flow, so a rejected promise surfaces as a thrown error your try can catch. A promise you don't await settles independently and bypasses the surrounding try entirely — you must use .catch() instead.
For deeper coverage of asynchronous error handling, see Error handling with promises and async/await.
Conclusion
Effective error handling in JavaScript is key to developing high-quality, resilient applications. Using try...catch lets you gracefully handle synchronous errors and awaited rejections while keeping control over application flow. Throw Error objects (never bare strings), branch on built-in types like TypeError and SyntaxError, rethrow what you can't handle, and remember the asynchronous limitation: errors in callbacks and unawaited promises need their own handling.
Related Topics
- Custom errors, extending Error — define your own error classes.
- Error handling with promises —
.catch()and rejection flow. - Async/await —
try...catchwith asynchronous code. - Working with JSON —
JSON.parseand theSyntaxErrorit can throw.