What Is Try-Catch? Error Handling Explained for AI Coders

When you ask AI to make an API call, fetch data, or connect to a database, it wraps the code in try-catch. Here's exactly what that means, why it matters, and how to use it when AI's version isn't working.

TL;DR

Try-catch is JavaScript's error handling system. The try block contains code that might fail. The catch block handles the failure gracefully instead of crashing your app. The finally block runs cleanup code no matter what. AI uses try-catch around every API call, database query, and file operation — places where external failures are out of your control.

Why AI Coders Need to Understand Try-Catch

Every time you ask AI to fetch data from an API, read a file, or query a database, it wraps the code in a try-catch block. This is correct — these operations can fail, and without try-catch, a single network timeout crashes your entire application.

But here's the problem: AI often generates catch blocks that silently swallow errors, making it impossible to debug when something goes wrong. Or it generates catch blocks that catch everything, hiding bugs in your own code that you should fix, not ignore.

Understanding try-catch means you can:

  • Read error messages instead of seeing a blank screen and no information
  • Know why an API call is failing and what to tell AI to fix
  • Improve the error handling AI generates to actually help you debug
  • Write your own error handling when AI's version doesn't fit your app

Real Scenario: Fetching Weather Data

Your AI Prompt

"Write a function that fetches weather data from the OpenWeatherMap API and returns the temperature for a given city."

What AI Generated

// weather.js
// Tested with Node.js 22.x, fetch API (native)

async function getWeather(city) {
  const API_KEY = process.env.WEATHER_API_KEY

  try {
    // This is the "try" block — code that might fail
    const response = await fetch(
      `https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${API_KEY}&units=metric`
    )

    // Check if the HTTP response is OK (status 200-299)
    if (!response.ok) {
      throw new Error(`Weather API returned ${response.status}: ${response.statusText}`)
    }

    // Parse the JSON — this can also fail if the API returns malformed data
    const data = await response.json()

    return {
      city: data.name,
      temperature: data.main.temp,
      description: data.weather[0].description
    }
  } catch (error) {
    // This is the "catch" block — runs if anything in "try" throws
    console.error('Failed to fetch weather:', error.message)
    return null  // Return null instead of crashing
  }
}

Let's understand every piece of this.

Understanding Each Part

The Try Block

The try block contains code that might throw an error. JavaScript executes it line by line. The moment any line throws an error, execution immediately jumps to the catch block — skipping all remaining lines in try.

try {
  // Line 1: runs
  const response = await fetch(url)
  // Line 2: only runs if Line 1 succeeded
  const data = await response.json()
  // Line 3: only runs if Lines 1 and 2 succeeded
  return data.main.temp
}

If fetch fails (network down, bad URL), lines 2 and 3 never run. JavaScript jumps straight to catch.

The Catch Block

The catch block receives the error as a parameter (usually named error or err). This error object has several useful properties:

catch (error) {
  console.log(error.message)  // "Failed to fetch" or "404 Not Found"
  console.log(error.name)     // "TypeError", "SyntaxError", "Error"
  console.log(error.stack)    // Full stack trace (where in the code it happened)
}

The error.message is the most useful for debugging — it tells you what went wrong. The error.stack tells you where in the code it happened.

Throwing Your Own Errors

Notice this line in the AI-generated code:

if (!response.ok) {
  throw new Error(`Weather API returned ${response.status}: ${response.statusText}`)
}

This is a manual throw. fetch itself doesn't throw on 404 or 500 errors — it just returns a response with ok: false. So AI manually checks and throws an error to make the catch block handle it. This is a pattern you'll see constantly in AI-generated code, and it's correct.

The Finally Block

async function getWeather(city) {
  let loadingSpinner = showSpinner()

  try {
    const data = await fetchWeatherData(city)
    return data
  } catch (error) {
    console.error('Weather fetch failed:', error.message)
    return null
  } finally {
    // Runs no matter what — success OR failure
    loadingSpinner.hide()
  }
}

finally always runs, whether the try succeeded or the catch handled an error. Use it for cleanup: hiding loading spinners, closing database connections, releasing resources.

Async/Await and Try-Catch

The combination of async/await and try-catch is the most common pattern in modern JavaScript. Without try-catch around an await, a rejected promise becomes an unhandled promise rejection that can crash Node.js or silently fail in browsers.

// BAD: No error handling — will crash or fail silently
async function badExample() {
  const data = await fetchData()  // If this fails, kaboom
  return data
}

// GOOD: Errors are caught and handled
async function goodExample() {
  try {
    const data = await fetchData()
    return data
  } catch (error) {
    console.error('Fetch failed:', error.message)
    return null
  }
}

What AI Gets Wrong About Try-Catch

⚠️ Most Common AI Mistakes

1. The Empty Catch Block (Silent Failure)

The most dangerous pattern AI generates:

// TERRIBLE: Error is swallowed. App appears to work. It's broken.
try {
  await saveToDatabase(data)
} catch (error) {
  // Nothing here. Error disappears. You'll never know it happened.
}

Fix: Always log errors in catch blocks. At minimum: console.error(error). Better: log to your error tracking system.

2. Catching Everything (Hiding Real Bugs)

// BAD: Catches bugs in your code, not just external failures
try {
  const result = processUserData(user)  // If this has a bug, catch hides it
  await saveToDatabase(result)
} catch (error) {
  console.log('Something went wrong')  // No idea what or where
}

When try-catch wraps too much code, you can't tell if an error came from your logic or from the database. Split them up:

// BETTER: Separate concerns
const result = processUserData(user)  // Let logic errors surface normally

try {
  await saveToDatabase(result)  // Only catch external failures
} catch (error) {
  console.error('Database save failed:', error.message)
}

3. Not Re-Throwing When Appropriate

// AI sometimes logs but doesn't tell the caller something failed
async function processOrder(orderId) {
  try {
    await chargeCard(orderId)
  } catch (error) {
    console.error('Charge failed:', error)
    // Returns undefined — caller thinks it succeeded!
  }
}

// Better: re-throw so the caller can handle it
async function processOrder(orderId) {
  try {
    await chargeCard(orderId)
  } catch (error) {
    console.error('Charge failed:', error)
    throw error  // Let the caller decide what to do
  }
}

How to Debug Try-Catch Errors with AI

When You See No Error (Silent Failure)

Debug Prompt

"My API call silently fails — I get null back but no error message. Here's the function: [paste code]. Add proper error logging to the catch block so I can see exactly what's going wrong, including the full error object."

When the Error Message Is Unhelpful

Debug Prompt

"I'm getting this error in my catch block: [paste error]. The error.message is just 'Failed to fetch'. What are the common causes of this and how do I get more specific information about what's failing?"

When Try-Catch Isn't Catching the Error

Debug Prompt

"My try-catch isn't catching this error — it's still crashing the app. Here's the code: [paste code]. The error is: [paste error]. Why isn't catch intercepting it?"

Common reason try-catch doesn't catch: You forgot await on an async function. Without await, the function returns a Promise immediately (before the error happens), and the error occurs later outside the try block.

// BUG: Missing await — try-catch won't catch async errors
try {
  fetchData()  // Returns Promise immediately, error happens later
} catch (error) {
  // This never catches the fetch error!
}

// FIX: Add await
try {
  await fetchData()  // Waits for completion — catches errors
} catch (error) {
  // Now this works
}

Useful Try-Catch Patterns

The Safe Fetch Wrapper

// A reusable wrapper that turns API errors into useful objects
async function safeFetch(url, options = {}) {
  try {
    const response = await fetch(url, options)

    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`)
    }

    return { data: await response.json(), error: null }
  } catch (error) {
    console.error(`Fetch failed for ${url}:`, error.message)
    return { data: null, error: error.message }
  }
}

// Usage
const { data, error } = await safeFetch('/api/users')
if (error) {
  showErrorMessage(error)
  return
}
renderUsers(data)

Multiple Try-Catch Blocks

// Use separate try-catch for independent operations
async function createUserAccount(userData) {
  let userId

  // Step 1: Create user in database
  try {
    const user = await db.user.create({ data: userData })
    userId = user.id
  } catch (error) {
    throw new Error(`Failed to create user: ${error.message}`)
  }

  // Step 2: Send welcome email (can fail without blocking account creation)
  try {
    await sendWelcomeEmail(userData.email)
  } catch (error) {
    console.warn('Welcome email failed (non-critical):', error.message)
    // Don't throw — email failure doesn't undo account creation
  }

  return userId
}

What to Learn Next

Frequently Asked Questions

What does try-catch do in JavaScript?

Try-catch runs the code in the 'try' block and, if that code throws an error, stops immediately and runs the 'catch' block instead of crashing the whole program. The catch block receives the error object, which contains the error type, message, and stack trace showing exactly where the failure happened.

When should you use try-catch?

Use try-catch around code that can fail for reasons outside your control: API calls, file system operations, database queries, JSON parsing, and external service calls. Don't use it to hide bugs in your own logic — let those errors surface so you can fix them. If you need try-catch around your own logic, it usually means the logic should be refactored.

What is async/await try-catch?

When using async/await, wrap your await calls in try-catch to handle errors. Without try-catch, an awaited promise that rejects will throw an unhandled error. The pattern is: try { const result = await someAsyncFunction(); } catch (error) { console.error(error); }. The await is critical — try-catch only works with awaited async functions.

What is the 'finally' block?

The 'finally' block runs after try and catch, regardless of whether an error occurred. It's used for cleanup code that must always run — like closing a database connection, clearing a loading spinner, or releasing a resource. Finally runs even if the catch block also throws an error, making it reliable for cleanup.

What does AI get wrong about error handling?

AI commonly generates empty catch blocks (swallowing errors silently), uses catch blocks too broadly (hiding actual bugs in your logic), doesn't re-throw errors after logging them (leaving callers unaware of failures), and sometimes wraps synchronous code that can't throw with try-catch unnecessarily. The most dangerous pattern is the empty catch block — always log the error at minimum.