Node.js & JavaScript Error Handling Best Practices | try/catch & async

Node.js & JavaScript Error Handling Best Practices | try/catch & async

이 글의 핵심

Production-oriented error handling: typed errors, async stacks, centralized handlers in Express, retries, and patterns that work the same in browsers and Node.js.

Introduction

Error handling deals with exceptional conditions that occur while your program runs.


1. try-catch-finally

Basics

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

Practical example

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("계산 완료");
}

Nested try-catch

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

2. The Error object

Built-in error types

// Error: generic
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 properties

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

3. Custom errors

Custom error classes

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

Handling multiple error types

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. Asynchronous errors

Promises

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

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

// Multiple promises
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. Practical patterns

Pattern 1: Result wrapper

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

Pattern 2: Retry with backoff

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

Pattern 3: Error logging

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));
        
        // Send to server
        // fetch('/api/errors', { method: 'POST', body: JSON.stringify(errorInfo) });
    }
}

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

6. Practical example: API client

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

Summary

Key takeaways

  1. try-catch-finally: structured error handling
  2. throw: signal failure
  3. Error object: name, message, stack
  4. Custom errors: class extends Error
  5. Async: .catch() or try/catch with async/await

Tips

  • Clear messages: make failures easy to diagnose
  • Branch by type: use instanceof (or error codes) for control flow
  • Re-throw: propagate with throw error when you cannot handle
  • Logging: record enough context for production debugging

Next steps

  • 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 완벽 활용