C
JavaScript/Error Handling/Lesson 18

Error Handling — try/catch/finally + Custom Errors

45 min·theory

Error Handling — try/catch/finally + Custom Errors

🎯 After reading this lesson

Once you finish this lesson, you'll be able to confidently do the following three things.

  • ✅ try / catch / finally + Error.cause (ES2022)
  • ✅ Separate domain exceptions with custom error classes
  • ✅ Error handling in async functions + unhandledrejection

Keep the learning objectives as a checklist — close the lesson once you can answer all of them.

try / catch / finally — Basic Structure

The Simplest try-catch

javascript
const rawInput = '{ "name": "Hong" }';   // Valid JSON
// const rawInput = 'broken string';     // ← Activating this line will run catch

try {
    const data = JSON.parse(rawInput);   // 💥 Throws here if it fails
    console.log('Success:', data);          // Success: { name: 'Hong' }
} catch (err) {
    console.error('Parsing failed:', err.message);
    // On failure → "Parsing failed: Unexpected token ... in JSON at ..."
}

// 💡 If a throw occurs inside try → immediately jumps to catch
//    If there's no throw, catch is skipped

When an error is thrown, the code above immediately jumps to the catch block. If nothing is thrown, catch is skipped.

finally — Runs Regardless of Success or Failure

javascript
let conn;
try {
    conn = await db.connect();
    return await conn.query('SELECT ...');
} catch (err) {
    log.error(err);
    throw err;     // Re-throw
} finally {
    if (conn) conn.release();   // Cleanup
}

The standard pattern for resource cleanup (database connections, files, locks).

4 Properties of the Error Object

javascript
try {
    null.foo;
} catch (e) {
    console.log(e.name);     // 'TypeError'
    console.log(e.message);  // "Cannot read properties of null..."
    console.log(e.stack);    // Call stack — *key for debugging*
    console.log(e.cause);    // Cause chaining (ES2022)
}

4 Common Error Types

  • TypeErrornull.foo, undefined() — the most frequently seen error
  • ReferenceError — referencing a variable that doesn't exist, like consle.log
  • SyntaxError — the syntax itself is broken (usually caught at build time)
  • RangeError — negative array length or infinite recursion

If you paste the error message directly into an AI, it will pinpoint the cause in most cases — that's the core of "saving debugging tokens."

Custom Error Classes — Separation by Domain

Why Custom Errors Are Needed

If you throw everything as a generic Error — branching by cause inside catch becomes difficult. Separate them with custom classes:

javascript
class AppError extends Error {
    constructor(message, code) {
        super(message);
        this.name = 'AppError';
        this.code = code;
    }
}

class NotFoundError extends AppError {
    constructor(resource) {
        super(`${resource} could not be found`, 'NOT_FOUND');
        this.statusCode = 404;
    }
}

class ValidationError extends AppError {
    constructor(field) {
        super(`${field} validation failed`, 'VALIDATION');
        this.statusCode = 400;
    }
}

Branching Logic

javascript
// 🧪 Fake function — throws different errors depending on input (copy-pasteable)
async function createUser(input) {
    if (!input.email) throw new ValidationError('email');
    if (input.id === 999) throw new NotFoundError('User');
    if (input.crash) throw new Error('DB down');
    return { ok: true };
}

// Simulation — mimics Express res object
const res = {
    status: (code) => ({
        json: (body) => console.log(`📤 HTTP ${code}`, body),
        end:  ()     => console.log(`📤 HTTP ${code}`)
    })
};
const log = { error: console.error };

// ▶️ Check branching with various inputs
(async () => {
    for (const input of [{ email: '' }, { id: 999, email: 'a' }, { crash: true, email: 'a' }]) {
        try {
            await createUser(input);
        } catch (e) {
            // 🔀 Respond differently depending on the error type
            if (e instanceof ValidationError) {
                res.status(400).json({ field: e.message });        // Bad input
            } else if (e instanceof NotFoundError) {
                res.status(404).end();                              // Not found
            } else {
                log.error('Unexpected:', e.message);
                res.status(500).end();                              // Server error
            }
        }
    }
})();

// 📤 Output:
//   HTTP 400 { field: 'email' }
//   HTTP 404
//   Unexpected: DB down
//   HTTP 500

// 💡 Check "if this error is an instance of which class" with instanceof
//    → Different handling possible for each type within the same catch block

Branch by type using instanceof. The intent of your code becomes clear.

ES2022 cause — Error Chaining

javascript
// 🧪 Fake low-level function — always fails
async function lowLevelCall() {
    throw new Error('Connection refused');
}

async function loadProduct() {
    try {
        await lowLevelCall();
    } catch (lowLevel) {
        // 🔗 "Chain" the original error as cause and wrap it in a new error
        throw new Error('Failed to retrieve product', { cause: lowLevel });
    }
}

(async () => {
    try {
        await loadProduct();
    } catch (e) {
        console.error(e.message);         // "Failed to retrieve product"
        console.error(e.cause.message);   // "Connection refused"   ← Original still alive!
    }
})();

Wraps a new error while preserving the original cause. The original stack trace remains in the logs, making debugging easier.

Error Handling in async / await — 3 Standard Patterns

Pattern 1 — try/catch in async

javascript
async function fetchUser(id) {
    try {
        const res = await fetch(`/api/users/${id}`);
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        return await res.json();
    } catch (err) {
        console.error('Failed:', err);
        return null;
    }
}

The most commonly used form. fetch does not reject on 4xx/5xx — you must check res.ok or res.status manually.

Pattern 2 — Promise.catch()

javascript
// 🧪 Fake function — fails after 50ms
const fetchUser = (id) => new Promise((_, reject) =>
    setTimeout(() => reject(new Error('User not found: ' + id)), 50)
);
const display = (u) => console.log('Display:', u);

fetchUser(1)
    .then(u   => display(u))
    .catch(err => console.error('Error:', err.message));   // 📤 "Error: User not found: 1"

Used in places that aren't async functions (event handlers, etc.). Can also be mixed with async/await.

Pattern 3 — Let the Error Propagate Up

javascript
async function getUser(id) {
    const res = await fetch(`/api/users/${id}`);
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return res.json();          // Error is delegated to the caller
}

// Caller handles it
try {
    const u = await getUser(1);
} catch (e) { ... }

Domain logic functions don't use try-catch. Responsibility is delegated to the top-level caller (controller or UI component).

A Common Pitfall — await Inside forEach

javascript
const items = [1, 2, 3];
const process = (n) => new Promise(r => setTimeout(() => {
    console.log('Processed:', n);
    r();
}, 100));

// ❌ forEach "does not wait" for the callback's Promise
items.forEach(async item => {
    await process(item);
});
console.log('Done');

// 📤 Output order:
//   Done              ← 🙀 forEach finishes immediately and prints first!
//   Processed: 1
//   Processed: 2
//   Processed: 3

forEach does not recognize Promises. Switch to for...of instead:

javascript
const items = [1, 2, 3];
const process = (n) => new Promise(r =>
    setTimeout(() => { console.log('Processing:', n); r(); }, 50)
);

(async () => {
    // ✅ Sequential — next starts only after one finishes (total 150ms)
    console.time('Sequential');
    for (const item of items) {
        await process(item);
    }
    console.timeEnd('Sequential');   // Sequential: ~150ms

    // ✅ Parallel — all three start simultaneously (total ~50ms)
    console.time('Parallel');
    await Promise.all(items.map(item => process(item)));
    console.timeEnd('Parallel');   // Parallel: ~50ms
})();

React Error Boundary — UI Error Handling

jsx
<ErrorBoundary fallback={<div>An error occurred</div>}>
    <MyComponent />
</ErrorBoundary>

Catches errors during rendering to prevent the entire app from crashing. try-catch cannot catch rendering errors.

🤖 Try asking AI like this

  • "Add a res.ok check and try-catch to this fetch code"
  • "Branch this error into NotFoundError and ValidationError handling"
  • "The await inside forEach isn't working — convert it to for...of"

⚡ Try It Yourself — try · catch · finally + Custom Errors

Branching error handling by error type. Use instanceof to identify what kind of error it is.
✏️ JS 코드
📟 Console output
▶ Press the Run button
⚠️ Runs in a browser sandbox — only console.log() is supported; alert/fetch are not.
Error Handling — try/catch/finally + Custom Errors - JavaScript