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
- try-catch-finally: structured error handling
- throw: signal failure
- Error object:
name,message,stack - Custom errors:
class extends Error - Async:
.catch()ortry/catchwithasync/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 errorwhen you cannot handle - Logging: record enough context for production debugging
Next steps
- JavaScript 디자인 패턴
- JavaScript 비동기
- TypeScript 시작하기
Related posts
- C++ 예외 처리 | try/catch/throw
- Java 예외 처리 | try-catch, throws, 커스텀 예외
- Python 예외 처리 | try-except, raise, 커스텀 예외 완벽 정리
- Swift 에러 처리 | do-catch, throw, Result
- C++ 디버깅 실전 가이드 | gdb, LLDB, Visual Studio 완벽 활용