C
JavaScript/에러처리/Lesson 18

에러 처리 — try/catch/finally + 커스텀 에러

45분·theory

에러 처리 — try/catch/finally + 커스텀 에러

🎯 이 lesson 을 읽고 나면

이 lesson 을 다 읽고 나면 아래 3가지를 자신 있게 할 수 있습니다.

  • ✅ try / catch / finally + Error.cause (ES2022)
  • ✅ 커스텀 에러 클래스로 도메인 예외 분리
  • ✅ async 함수의 에러 처리 + unhandledrejection

학습 목표를 체크리스트로 두고 다 답할 수 있게 되면 lesson 을 닫으세요.

try / catch / finally — 기본 구조

가장 단순한 try-catch

javascript
const rawInput = '{ "name": "홍" }';   // 올바른 JSON
// const rawInput = '깨진 문자열';     // ← 이 줄을 활성화하면 catch 가 실행됨

try {
    const data = JSON.parse(rawInput);   // 💥 실패하면 여기서 throw
    console.log('성공:', data);          // 성공: { name: '홍' }
} catch (err) {
    console.error('파싱 실패:', err.message);
    // 실패 시 → "파싱 실패: Unexpected token ... in JSON at ..."
}

// 💡 try 안에서 throw 가 나면 → 즉시 catch 로 점프
//    throw 가 없으면 catch 는 건너뜀

에러가 던져지면 위 코드는 즉시 catch 블록으로 점프. 던지지 않으면 catch 는 건너뜀.

finally — 성공·실패 상관없이 실행

javascript
let conn;
try {
    conn = await db.connect();
    return await conn.query('SELECT ...');
} catch (err) {
    log.error(err);
    throw err;     // 다시 던지기
} finally {
    if (conn) conn.release();   // 정리
}

리소스 해제 (DB 커넥션·파일·락) 의 표준 패턴.

Error 객체의 4가지 정보

javascript
try {
    null.foo;
} catch (e) {
    console.log(e.name);     // 'TypeError'
    console.log(e.message);  // "Cannot read properties of null..."
    console.log(e.stack);    // 호출 스택 — *디버깅의 핵심*
    console.log(e.cause);    // 원인 체이닝 (ES2022)
}

흔한 에러 타입 4종

  • TypeErrornull.foo, undefined() — 가장 자주 보는 에러
  • ReferenceErrorconsle.log 같은 없는 변수 참조
  • SyntaxError — 문법 자체가 깨짐 (보통 빌드 단계에서)
  • RangeError — 배열 길이 음수·재귀 무한 호출

AI 에게 에러 메시지를 그대로 보여주면 대부분 원인을 짚어줍니다 — 이게 "디버깅 토큰 절약" 의 핵심.

커스텀 에러 클래스 — 도메인별 분리

왜 커스텀 에러가 필요한가

모든 에러를 Error 하나로 던지면 — catch 에서 원인별 분기 가 어려움. 커스텀 클래스로 분리:

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

class NotFoundError extends AppError {
    constructor(resource) {
        super(`${resource} 를 찾을 수 없습니다`, 'NOT_FOUND');
        this.statusCode = 404;
    }
}

class ValidationError extends AppError {
    constructor(field) {
        super(`${field} 검증 실패`, 'VALIDATION');
        this.statusCode = 400;
    }
}

분기 처리

javascript
// 🧪 가짜 함수 — 입력에 따라 다른 에러 throw (복붙 가능)
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 다운');
    return { ok: true };
}

// 시뮬레이션 — Express res 객체 흉내
const res = {
    status: (code) => ({
        json: (body) => console.log(`📤 HTTP ${code}`, body),
        end:  ()     => console.log(`📤 HTTP ${code}`)
    })
};
const log = { error: console.error };

// ▶️ 다양한 입력으로 분기 확인
(async () => {
    for (const input of [{ email: '' }, { id: 999, email: 'a' }, { crash: true, email: 'a' }]) {
        try {
            await createUser(input);
        } catch (e) {
            // 🔀 어떤 에러냐에 따라 다르게 응답
            if (e instanceof ValidationError) {
                res.status(400).json({ field: e.message });        // 입력 잘못
            } else if (e instanceof NotFoundError) {
                res.status(404).end();                              // 없음
            } else {
                log.error('Unexpected:', e.message);
                res.status(500).end();                              // 서버 잘못
            }
        }
    }
})();

// 📤 출력:
//   HTTP 400 { field: 'email' }
//   HTTP 404
//   Unexpected: DB 다운
//   HTTP 500

// 💡 instanceof 로 "이 에러가 어떤 클래스의 인스턴스인가" 확인
//    → 같은 catch 블록에서 종류별로 다른 처리 가능

instanceof 로 타입 분기. 코드 의도가 명확해집니다.

ES2022 cause — 에러 체이닝

javascript
// 🧪 가짜 저수준 함수 — 항상 실패
async function lowLevelCall() {
    throw new Error('Connection refused');
}

async function loadProduct() {
    try {
        await lowLevelCall();
    } catch (lowLevel) {
        // 🔗 원본 에러를 cause 로 "체이닝" 해서 새 에러로 감쌈
        throw new Error('상품 조회 실패', { cause: lowLevel });
    }
}

(async () => {
    try {
        await loadProduct();
    } catch (e) {
        console.error(e.message);         // "상품 조회 실패"
        console.error(e.cause.message);   // "Connection refused"   ← 원본 살아있음!
    }
})();

원인 에러를 보존 한 채 새 에러로 감쌈. 로그에 원본 stack 까지 남아 디버깅이 쉬워집니다.

async / await 에서 에러 처리 — 표준 3패턴

패턴 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('실패:', err);
        return null;
    }
}

가장 자주 쓰는 형태. fetch 는 4xx/5xx 에서 reject 하지 않음res.ok 또는 res.status 를 직접 검사해야 합니다.

패턴 2 — Promise.catch()

javascript
// 🧪 가짜 함수 — 50ms 후 실패
const fetchUser = (id) => new Promise((_, reject) =>
    setTimeout(() => reject(new Error('User not found: ' + id)), 50)
);
const display = (u) => console.log('표시:', u);

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

async 함수가 아닌 곳 (이벤트 핸들러 등) 에서 사용. async/await 와 혼용도 가능.

패턴 3 — 상위로 그대로 던지기

javascript
async function getUser(id) {
    const res = await fetch(`/api/users/${id}`);
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return res.json();          // 에러는 호출자에게 위임
}

// 호출자가 처리
try {
    const u = await getUser(1);
} catch (e) { ... }

도메인 로직 함수는 try-catch 안 함. 책임은 최상위 호출자 (컨트롤러·UI 컴포넌트) 에 위임.

흔한 함정 — forEach 에서 await

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

// ❌ forEach 는 콜백의 Promise 를 "기다리지 않음"
items.forEach(async item => {
    await process(item);
});
console.log('완료');

// 📤 출력 순서:
//   완료              ← 🙀 forEach 가 즉시 끝나버려서 먼저 찍힘!
//   처리됨: 1
//   처리됨: 2
//   처리됨: 3

forEach 는 Promise 를 인식하지 않음. for...of 로 바꾸세요:

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

(async () => {
    // ✅ 순차 — 하나 끝나야 다음 시작 (총 150ms)
    console.time('순차');
    for (const item of items) {
        await process(item);
    }
    console.timeEnd('순차');   // 순차: ~150ms

    // ✅ 병렬 — 셋 동시 시작 (총 ~50ms)
    console.time('병렬');
    await Promise.all(items.map(item => process(item)));
    console.timeEnd('병렬');   // 병렬: ~50ms
})();

React Error Boundary — UI 에러 처리

jsx
<ErrorBoundary fallback={<div>에러 발생</div>}>
    <MyComponent />
</ErrorBoundary>

렌더링 중 에러 를 잡아 전체 앱이 빠지지 않게 막아줌. try-catch 는 렌더링 에러를 못 잡습니다.

🤖 AI 에게 이렇게 요청해보세요

  • "이 fetch 코드에 res.ok 검사와 try-catch 를 추가해줘"
  • "이 에러를 NotFoundError·ValidationError 로 분기해서 처리해줘"
  • "forEach 의 await 가 동작 안 하는데 for...of 로 바꿔줘"

⚡ 직접 해보기 — try · catch · finally + 커스텀 에러

에러 종류별 분기 처리. instanceof 로 어떤 에러인지 판별.
✏️ JS 코드
📟 콘솔 출력
▶ 실행 버튼을 눌러보세요
⚠️ 브라우저 샌드박스에서 실행 — console.log()만 지원, alert/fetch 불가
먼저 읽으면 좋은 개념: Async/Await
에러 처리 — try/catch/finally + 커스텀 에러 - JavaScript