docs/concepts/error-handling.mdx
What happens when something goes wrong in your JavaScript code? How do you prevent one small error from crashing your entire application? How do you give users helpful feedback instead of a cryptic error message?
// Without error handling - your app crashes
const userData = JSON.parse('{ invalid json }') // SyntaxError!
// With error handling - you stay in control
try {
const userData = JSON.parse('{ invalid json }')
} catch (error) {
console.log('Could not parse user data:', error.message)
// Show user a friendly message, use default data, etc.
}
Error handling is how you detect, respond to, and recover from errors in your code. JavaScript provides the try...catch...finally statement for synchronous errors and special patterns for handling async errors in Promises and async/await.
Errors happen. Users enter invalid data, network requests fail, APIs return unexpected responses, and sometimes we just make typos. Error handling is your strategy for detecting, responding to, and recovering from these problems gracefully. In JavaScript, you use the try...catch statement to catch errors, the throw statement to create them, and the Error object to describe what went wrong. According to the Stack Overflow 2023 Developer Survey, debugging and error handling remain among the most time-consuming aspects of development, making robust error handling patterns a critical skill.
Think of error handling like a trapeze act at a circus. The acrobat (your code) performs risky moves high above the ground. The safety net (your catch block) is there to catch them if they fall. And no matter what happens, the show must go on (your finally block).
┌─────────────────────────────────────────────────────────────────────────┐
│ THE SAFETY NET ANALOGY │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ try { TRAPEZE ACT │
│ riskyMove() ┌─────────┐ │
│ } │ ACROBAT │ ← Your risky code │
│ └────┬────┘ │
│ │ │
│ catch (error) { ▼ FALLS! │
│ recover() ═══════════════════════ │
│ } SAFETY NET ← Catches the error │
│ │
│ finally { The show continues! │
│ cleanup() (runs no matter what) │
│ } │
│ │
└─────────────────────────────────────────────────────────────────────────┘
| Circus | JavaScript | Purpose |
|---|---|---|
| Trapeze act | try block | Code that might fail |
| Safety net | catch block | Handles the error if one occurs |
| Show continues | finally block | Cleanup that always runs |
| Acrobat falls | Error is thrown | Something went wrong |
The try...catch statement is JavaScript's primary tool for handling errors. As MDN documents, this statement has been part of JavaScript since ECMAScript 3 (1999) and remains the standard mechanism for synchronous error handling. Here's the full syntax:
try {
// Code that might throw an error
const result = riskyOperation()
console.log(result)
} catch (error) {
// Code that runs if an error is thrown
console.error('Something went wrong:', error.message)
} finally {
// Code that ALWAYS runs, error or not
cleanup()
}
The try block contains code that might throw an error. If an error occurs, execution immediately jumps to the catch block.
try {
console.log('Starting...') // Runs
JSON.parse('{ bad json }') // Error! Jump to catch
console.log('This never runs') // Skipped
}
The catch block receives the error object and handles it. This is where you log errors, show user messages, or attempt recovery.
try {
const data = JSON.parse(userInput)
} catch (error) {
// error contains information about what went wrong
console.log(error.name) // "SyntaxError"
console.log(error.message) // "Unexpected token b in JSON..."
// You can recover gracefully
const data = { fallback: true }
}
try {
JSON.parse(maybeJson)
} catch {
// No (error) parameter needed if you don't use it
return null
}
The finally block always runs, whether an error occurred or not. It's perfect for cleanup code like closing connections or hiding loading spinners.
let isLoading = true
try {
const data = await fetchData()
displayData(data)
} catch (error) {
showErrorMessage(error)
} finally {
// This runs no matter what!
isLoading = false
hideLoadingSpinner()
}
function example() {
try {
return 'from try'
} finally {
console.log('finally runs!') // This still logs!
}
}
example() // Logs "finally runs!", then returns "from try"
This trips people up: try/catch won't catch errors in callbacks that run later.
// ❌ WRONG - catch won't catch this error!
try {
setTimeout(() => {
throw new Error('Async error')
}, 1000)
} catch (error) {
console.log('This never runs')
}
// ✓ CORRECT - try/catch inside the callback
setTimeout(() => {
try {
throw new Error('Async error')
} catch (error) {
console.log('Caught:', error.message)
}
}, 1000)
For async code, see the Async Error Handling section.
When an error occurs, JavaScript creates an Error object with information about what went wrong.
| Property | Description | Example |
|---|---|---|
name | The type of error | "TypeError", "ReferenceError" |
message | Human-readable description | "Cannot read property 'x' of undefined" |
stack | Call stack when error occurred (non-standard but widely supported) | Shows file names, line numbers |
cause | Original error (ES2022+) | Used for error chaining |
try {
undefinedVariable
} catch (error) {
console.log(error.name) // "ReferenceError"
console.log(error.message) // "undefinedVariable is not defined"
console.log(error.stack) // Full stack trace with line numbers
}
The stack property is essential for debugging. It shows exactly where the error occurred and the chain of function calls that led to it.
JavaScript has several built-in error types. Knowing them helps you understand what went wrong and how to fix it. The ECMAScript specification defines seven native error types, each representing a different category of runtime problem.
| Error Type | When It Occurs | Common Cause |
|---|---|---|
| Error | Generic error | Base class, used for custom errors |
| TypeError | Wrong type | null.foo, calling non-function |
| ReferenceError | Invalid reference | Using undefined variable |
| SyntaxError | Invalid syntax | Bad JSON, missing brackets |
| RangeError | Value out of range | new Array(-1) |
| URIError | Bad URI encoding | decodeURIComponent('%') |
| AggregateError | Multiple errors | Promise.any() all reject |
```javascript
const user = null
console.log(user.name) // TypeError: Cannot read property 'name' of null
const notAFunction = 42
notAFunction() // TypeError: notAFunction is not a function
```
**Fix:** Check if values exist before using them:
```javascript
console.log(user?.name) // undefined (no error)
```
```javascript
console.log(userName) // ReferenceError: userName is not defined
```
**Common causes:** Typos in variable names, forgetting to import, using variables before declaration.
```javascript
JSON.parse('{ name: "John" }') // SyntaxError: Unexpected token n
// JSON requires double quotes: { "name": "John" }
JSON.parse('') // SyntaxError: Unexpected end of JSON input
```
**Note:** Syntax errors in your source code are caught at parse time, not runtime. `try/catch` only catches runtime SyntaxErrors like invalid JSON.
```javascript
new Array(-1) // RangeError: Invalid array length
(1.5).toFixed(200) // RangeError: precision out of range (max is 100)
'x'.repeat(Infinity) // RangeError: Invalid count value
```
The throw statement lets you create your own errors. When you throw, execution stops and jumps to the nearest catch block.
function divide(a, b) {
if (b === 0) {
throw new Error('Cannot divide by zero')
}
return a / b
}
try {
const result = divide(10, 0)
} catch (error) {
console.log(error.message) // "Cannot divide by zero"
}
Technically you can throw anything, but always throw Error objects. They include a stack trace for debugging.
// ❌ BAD - No stack trace, hard to debug
throw 'Something went wrong'
throw 404
throw { message: 'Error' }
// ✓ GOOD - Includes stack trace
throw new Error('Something went wrong')
throw new TypeError('Expected a string')
throw new RangeError('Value must be between 0 and 100')
Good error messages tell you what went wrong and ideally how to fix it:
// ❌ Vague
throw new Error('Invalid input')
// ✓ Specific
throw new Error('Email address is invalid: missing @ symbol')
throw new TypeError(`Expected string but got ${typeof value}`)
throw new RangeError(`Age must be between 0 and 150, got ${age}`)
For larger applications, create custom error classes to categorize errors and add extra information.
class ValidationError extends Error {
constructor(message) {
super(message)
this.name = 'ValidationError'
}
}
class NetworkError extends Error {
constructor(message, statusCode) {
super(message)
this.name = 'NetworkError'
this.statusCode = statusCode
}
}
Instead of manually setting this.name in every class, use the constructor name:
class AppError extends Error {
constructor(message, options) {
super(message, options)
this.name = this.constructor.name // Automatically uses class name
}
}
class ValidationError extends AppError {}
class DatabaseError extends AppError {}
class NetworkError extends AppError {}
// All have correct names automatically
throw new ValidationError('Invalid email') // error.name === "ValidationError"
Custom errors let you handle different error types differently:
try {
await saveUser(userData)
} catch (error) {
if (error instanceof ValidationError) {
// Show validation message to user
showFieldErrors(error.fields)
} else if (error instanceof NetworkError) {
// Network issue - maybe retry
showRetryButton()
} else {
// Unknown error - log and show generic message
console.error('Unexpected error:', error)
showGenericError()
}
}
When catching and re-throwing errors, preserve the original error using the cause option:
async function fetchUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`)
return await response.json()
} catch (error) {
// Wrap the original error with more context
throw new Error(`Failed to load user ${userId}`, { cause: error })
}
}
// Later, you can access the original error
try {
await fetchUserData(123)
} catch (error) {
console.log(error.message) // "Failed to load user 123"
console.log(error.cause.message) // Original fetch error
}
Error handling works differently with asynchronous code. Here's a quick overview. For comprehensive coverage, see our Promises and async/await guides.
Use .catch() to handle errors in Promise chains:
fetch('/api/users')
.then(response => response.json())
.then(users => displayUsers(users))
.catch(error => {
// Catches errors from fetch, json parsing, or displayUsers
console.error('Failed to load users:', error)
})
.finally(() => {
hideLoadingSpinner()
})
With async/await, use regular try/catch blocks:
async function loadUsers() {
try {
const response = await fetch('/api/users')
const users = await response.json()
return users
} catch (error) {
console.error('Failed to load users:', error)
throw error // Re-throw if caller should handle it
}
}
This catches many developers off guard: fetch() doesn't throw on HTTP errors like 404 or 500. It only throws on network failures.
// ❌ WRONG - This won't catch 404 or 500 errors!
try {
const response = await fetch('/api/users/999')
const user = await response.json() // Might fail on error response
} catch (error) {
// Only catches network errors, not HTTP errors
}
// ✓ CORRECT - Check response.ok
try {
const response = await fetch('/api/users/999')
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`)
}
const user = await response.json()
} catch (error) {
// Now catches both network AND HTTP errors
console.error('Request failed:', error.message)
}
Global error handlers catch errors that slip through your try/catch blocks. They're a safety net of last resort, not a replacement for proper error handling.
Catches uncaught errors in the browser:
window.onerror = function(message, source, lineno, colno, error) {
console.log('Uncaught error:', message)
console.log('Source:', source, 'Line:', lineno)
// Send to error tracking service
logErrorToService(error)
// Return true to prevent default browser error handling
return true
}
Catches unhandled Promise rejections:
window.addEventListener('unhandledrejection', event => {
console.warn('Unhandled promise rejection:', event.reason)
// Prevent the default browser warning
event.preventDefault()
// Log to error tracking service
logErrorToService(event.reason)
})
Not for: Regular error handling. Always prefer specific try/catch blocks. </Tip>
// ❌ WRONG - Error is silently lost
try {
riskyOperation()
} catch (error) {
// Nothing here - you'll never know something failed
}
// ✓ CORRECT - At minimum, log the error
try {
riskyOperation()
} catch (error) {
console.error('Operation failed:', error)
}
// ❌ WRONG - Hides programming bugs
try {
processData(data)
undefinedVriable // Typo! This bug is now hidden
} catch (error) {
return 'Something went wrong'
}
// ✓ CORRECT - Only catch expected errors
try {
return JSON.parse(userInput)
} catch (error) {
if (error instanceof SyntaxError) {
return null // Expected: invalid JSON
}
throw error // Unexpected: re-throw
}
// ❌ WRONG - No stack trace
throw 'User not found'
// ✓ CORRECT - Has stack trace for debugging
throw new Error('User not found')
// ❌ WRONG - Caller doesn't know an error occurred
async function fetchData() {
try {
return await fetch('/api/data')
} catch (error) {
console.log('Error:', error)
// Returns undefined - caller thinks it succeeded!
}
}
// ✓ CORRECT - Re-throw or return meaningful value
async function fetchData() {
try {
return await fetch('/api/data')
} catch (error) {
console.log('Error:', error)
throw error // Let caller handle it
// OR: return null with explicit meaning
}
}
// ❌ WRONG - Won't catch async errors
try {
setTimeout(() => {
throw new Error('Async error') // Uncaught!
}, 1000)
} catch (error) {
console.log('Never runs')
}
// ✓ CORRECT - Put try/catch inside callback
setTimeout(() => {
try {
throw new Error('Async error')
} catch (error) {
console.log('Caught:', error.message)
}
}, 1000)
Automatically retry failed operations, useful for flaky network requests:
async function fetchWithRetry(url, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
const response = await fetch(url)
if (!response.ok) throw new Error(`HTTP ${response.status}`)
return await response.json()
} catch (error) {
if (i === retries - 1) throw error // Last attempt, give up
// Wait before retrying (exponential backoff)
await new Promise(r => setTimeout(r, 1000 * Math.pow(2, i)))
}
}
}
Collect multiple validation errors at once:
class ValidationError extends Error {
constructor(errors) {
super('Validation failed')
this.name = 'ValidationError'
this.errors = errors // { email: "Invalid email", age: "Must be positive" }
}
}
function validateUser(data) {
const errors = {}
if (!data.email?.includes('@')) {
errors.email = 'Invalid email address'
}
if (data.age < 0) {
errors.age = 'Age must be positive'
}
if (Object.keys(errors).length > 0) {
throw new ValidationError(errors)
}
}
// Usage
try {
validateUser({ email: 'bad', age: -5 })
} catch (error) {
if (error instanceof ValidationError) {
// Show errors next to form fields
Object.entries(error.errors).forEach(([field, message]) => {
showFieldError(field, message)
})
}
}
Try the ideal path, fall back to alternatives:
async function loadUserPreferences(userId) {
try {
// Try to fetch from API
return await fetchFromApi(`/preferences/${userId}`)
} catch (apiError) {
console.warn('API unavailable, trying cache:', apiError.message)
try {
// Fall back to local storage
const cached = localStorage.getItem(`prefs_${userId}`)
if (cached) return JSON.parse(cached)
} catch (cacheError) {
console.warn('Cache unavailable:', cacheError.message)
}
// Fall back to defaults
return { theme: 'light', language: 'en' }
}
}
Use try/catch for synchronous code — Wrap risky operations and handle errors appropriately
try/catch is synchronous — It won't catch errors in callbacks. Use .catch() for Promises or try/catch inside async functions
Always throw Error objects, not strings — Error objects include stack traces that are essential for debugging
Always check response.ok with fetch — fetch() doesn't throw on HTTP errors like 404 or 500
Create custom Error classes — They help categorize errors and add context for better handling
Use finally for cleanup — Code in finally always runs, perfect for hiding spinners or closing connections
Don't swallow errors — Empty catch blocks hide bugs. Always log or re-throw
Use error.cause for chaining — Preserve original errors when wrapping them with more context
Re-throw errors you can't handle — If you catch an error you didn't expect, re-throw it
Use global handlers as a safety net — They're for logging and tracking, not for regular error handling
</Info>`try/catch` only catches **synchronous** errors. If you have async code inside the try block (like setTimeout callbacks), errors won't be caught.
Promise `.catch()` catches **Promise rejections**, which are async. With async/await, you can use try/catch because `await` converts rejections to thrown errors.
```javascript
// try/catch with async/await - works!
try {
await fetch('/api/data')
} catch (error) {
// Catches rejections because await converts them
}
// try/catch with callbacks - doesn't work!
try {
setTimeout(() => { throw new Error() }, 1000)
} catch (error) {
// Never runs - the error is thrown later
}
```
`fetch()` only throws on **network failures** (can't reach the server). HTTP errors like 404 (Not Found) or 500 (Server Error) are valid HTTP responses, so `fetch()` resolves successfully.
You must check `response.ok` to detect HTTP errors:
```javascript
const response = await fetch('/api/users/999')
if (!response.ok) {
// 404, 500, etc.
throw new Error(`HTTP error: ${response.status}`)
}
const data = await response.json()
```
Error objects include a **stack trace** showing where the error occurred and the chain of function calls. Strings don't have this information.
```javascript
throw 'Something went wrong' // No stack trace
throw new Error('Something went wrong') // Has stack trace
```
The stack trace is essential for debugging, especially in production where you can't use a debugger.
The `finally` block **always runs**, whether an error occurred or not, and even if there's a `return` statement in try or catch. It's ideal for cleanup code.
```javascript
function example() {
try {
return 'success'
} catch (error) {
return 'error'
} finally {
console.log('Cleanup!') // Always runs!
}
}
example() // Logs "Cleanup!" then returns "success"
```
Use it for: hiding loading spinners, closing connections, releasing resources.
Use `instanceof` to check the error type, or check `error.name`:
```javascript
try {
riskyOperation()
} catch (error) {
if (error instanceof TypeError) {
console.log('Type error:', error.message)
} else if (error instanceof SyntaxError) {
console.log('Syntax error:', error.message)
} else {
// Unknown error - re-throw it
throw error
}
}
```
This is especially useful with custom error classes:
```javascript
if (error instanceof ValidationError) {
showFormErrors(error.errors)
} else if (error instanceof NetworkError) {
showOfflineMessage()
}
```
console.log(result) // ???
```
**Answer:**
`result` is scoped to the try block. It doesn't exist outside of it, so `console.log(result)` throws a ReferenceError.
**Fix:** Declare the variable outside the try block:
```javascript
let result
try {
result = riskyOperation()
} catch (e) {
result = 'fallback value'
}
console.log(result) // Works!
```