JavaScript 에러 처리 | try-catch, Error 객체, 커스텀 에러

JavaScript 에러 처리 | try-catch, Error 객체, 커스텀 에러

이 글의 핵심

JavaScript 에러 처리에 대한 실전 가이드입니다. try-catch, Error 객체, 커스텀 에러 등을 예제와 함께 설명합니다.

들어가며

에러 처리는 실행 중 실패할 수 있는 코드에서 사용자·로그·복구 경로를 정하는 일입니다. try/catch로 잡을지, Promise의 catch로 이어질지, 에러 타입을 나눌지까지 이 글에서 정리합니다.


1. try-catch-finally

기본 사용법

try {
    const result = riskyOperation();
    console.log(result);
} catch (error) {
    console.error("에러 발생:", error.message);
} finally {
    console.log("정리 작업");
}

실전 예제

function divide(a, b) {
    if (b === 0) {
        throw new Error("0으로 나눌 수 없습니다");
    }
    return a / b;
}

try {
    console.log(divide(10, 2));  // 5
    console.log(divide(10, 0));  // Error!
    console.log("이 줄은 실행 안 됨");
} catch (error) {
    console.error("에러:", error.message);
} finally {
    console.log("계산 완료");
}

중첩 try-catch

try {
    try {
        throw new Error("내부 에러");
    } catch (innerError) {
        console.log("내부 처리:", innerError.message);
        throw new Error("외부 에러");
    }
} catch (outerError) {
    console.log("외부 처리:", outerError.message);
}

2. Error 객체

내장 Error 타입

// Error: 기본 에러
throw new Error("일반 에러");

// SyntaxError: 문법 에러
try {
    eval("{ invalid json");
} catch (e) {
    console.log(e.name);  // SyntaxError
}

// ReferenceError: 존재하지 않는 변수
try {
    console.log(nonExistent);
} catch (e) {
    console.log(e.name);  // ReferenceError
}

// TypeError: 타입 에러
try {
    null.toString();
} catch (e) {
    console.log(e.name);  // TypeError
}

// RangeError: 범위 에러
try {
    new Array(-1);
} catch (e) {
    console.log(e.name);  // RangeError
}

Error 객체 속성

try {
    throw new Error("테스트 에러");
} catch (error) {
    console.log(error.name);     // Error
    console.log(error.message);  // 테스트 에러
    console.log(error.stack);    // 스택 트레이스
}

3. 커스텀 에러

커스텀 에러 클래스

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;
    }
}

function validateAge(age) {
    if (typeof age !== 'number') {
        throw new ValidationError("나이는 숫자여야 합니다");
    }
    if (age < 0 || age > 150) {
        throw new ValidationError("나이는 0-150 사이여야 합니다");
    }
    return true;
}

try {
    validateAge("25");
} catch (error) {
    if (error instanceof ValidationError) {
        console.error("유효성 에러:", error.message);
    } else {
        console.error("알 수 없는 에러:", error);
    }
}

여러 에러 타입 처리

class DatabaseError extends Error {
    constructor(message, query) {
        super(message);
        this.name = "DatabaseError";
        this.query = query;
    }
}

function processData(data) {
    try {
        if (!data) {
            throw new ValidationError("데이터가 없습니다");
        }
        
        if (data.age < 0) {
            throw new ValidationError("나이는 양수여야 합니다");
        }
        
        return data;
    } catch (error) {
        if (error instanceof ValidationError) {
            console.error("유효성 에러:", error.message);
        } else if (error instanceof DatabaseError) {
            console.error("DB 에러:", error.message, error.query);
        } else {
            console.error("알 수 없는 에러:", error);
        }
        throw error;
    }
}

4. 비동기 에러 처리

Promise 에러

// .catch()
fetch("https://api.example.com/data")
    .then(response => response.json())
    .then(data => console.log(data))
    .catch(error => console.error("에러:", error));

// 체인 중간 에러
Promise.resolve(1)
    .then(x => {
        throw new Error("에러!");
    })
    .then(x => console.log(x))
    .catch(error => console.error(error.message))
    .then(() => console.log("복구됨"));

// 여러 Promise
Promise.all([
    fetch("/api/users"),
    fetch("/api/posts")
])
.then(responses => Promise.all(responses.map(r => r.json())))
.then(data => console.log(data))
.catch(error => console.error("하나라도 실패:", error));

async/await 에러

async function fetchData() {
    try {
        const response = await fetch("https://api.example.com/data");
        
        if (!response.ok) {
            throw new NetworkError(`HTTP ${response.status}`, response.status);
        }
        
        const data = await response.json();
        return data;
    } catch (error) {
        console.error("에러:", error.message);
        return null;
    }
}

async function complexOperation() {
    try {
        const data = await fetchData();
        const result = processData(data);
        return result;
    } catch (error) {
        if (error instanceof NetworkError) {
            console.error("네트워크 에러:", error.statusCode);
        } else if (error instanceof ValidationError) {
            console.error("유효성 에러:", error.message);
        } else {
            console.error("알 수 없는 에러:", error);
        }
        throw error;
    }
}

5. 실전 패턴

패턴 1: 에러 래퍼

class Result {
    constructor(success, data, error) {
        this.success = success;
        this.data = data;
        this.error = error;
    }
    
    static ok(data) {
        return new Result(true, data, null);
    }
    
    static fail(error) {
        return new Result(false, null, error);
    }
}

async function fetchUserSafe(id) {
    try {
        const response = await fetch(`/api/users/${id}`);
        const user = await response.json();
        return Result.ok(user);
    } catch (error) {
        return Result.fail(error.message);
    }
}

const result = await fetchUserSafe(1);
if (result.success) {
    console.log("데이터:", result.data);
} else {
    console.error("에러:", result.error);
}

패턴 2: 재시도

async function retry(fn, maxRetries = 3, delay = 1000) {
    for (let i = 0; i < maxRetries; i++) {
        try {
            return await fn();
        } catch (error) {
            if (i === maxRetries - 1) {
                throw error;
            }
            console.log(`재시도 ${i + 1}/${maxRetries}`);
            await new Promise(resolve => setTimeout(resolve, delay * (i + 1)));
        }
    }
}

retry(() => fetch("https://api.example.com/data"))
    .then(response => response.json())
    .then(data => console.log(data))
    .catch(error => console.error("최종 실패:", error));

패턴 3: 에러 로깅

class ErrorLogger {
    static log(error, context = {}) {
        const errorInfo = {
            name: error.name,
            message: error.message,
            stack: error.stack,
            timestamp: new Date().toISOString(),
            ...context
        };
        
        console.error("에러 로그:", JSON.stringify(errorInfo, null, 2));
        
        // 서버로 전송
        // fetch('/api/errors', { method: 'POST', body: JSON.stringify(errorInfo) });
    }
}

try {
    throw new Error("테스트 에러");
} catch (error) {
    ErrorLogger.log(error, { userId: 123, action: "데이터 로드" });
}

6. 실전 예제

예제: API 클라이언트

class ApiClient {
    constructor(baseUrl) {
        this.baseUrl = baseUrl;
    }
    
    async request(endpoint, options = {}) {
        const url = `${this.baseUrl}${endpoint}`;
        
        try {
            const response = await fetch(url, options);
            
            if (!response.ok) {
                throw new NetworkError(
                    `HTTP ${response.status}: ${response.statusText}`,
                    response.status
                );
            }
            
            const data = await response.json();
            return Result.ok(data);
        } catch (error) {
            if (error instanceof NetworkError) {
                console.error("네트워크 에러:", error.message);
            } else if (error instanceof SyntaxError) {
                console.error("JSON 파싱 에러:", error.message);
            } else {
                console.error("알 수 없는 에러:", error);
            }
            return Result.fail(error.message);
        }
    }
    
    async get(endpoint) {
        return this.request(endpoint);
    }
    
    async post(endpoint, body) {
        return this.request(endpoint, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(body)
        });
    }
}

const api = new ApiClient("https://api.example.com");

const result = await api.get("/users/1");
if (result.success) {
    console.log("사용자:", result.data);
} else {
    console.error("에러:", result.error);
}

정리

핵심 요약

  1. try-catch-finally: 에러 처리 구조
  2. throw: 에러 발생
  3. Error 객체: name, message, stack
  4. 커스텀 에러: extends Error
  5. 비동기: .catch() 또는 try-catch (async/await)

에러 처리 팁

  • 명확한 에러 메시지: 문제를 쉽게 파악
  • 에러 타입 구분: instanceof로 처리 분기
  • 재발생: throw error로 상위로 전파
  • 로깅: 에러 정보 기록

다음 단계

  • JavaScript 디자인 패턴
  • JavaScript 비동기
  • TypeScript 시작하기

관련 글

  • C++ 예외 처리 | try/catch/throw
  • Java 예외 처리 | try-catch, throws, 커스텀 예외
  • Python 예외 처리 | try-except, raise, 커스텀 예외 완벽 정리
  • Swift 에러 처리 | do-catch, throw, Result
  • C++ 디버깅 실전 가이드 | gdb, LLDB, Visual Studio 완벽 활용