개인의 기록
  • 소개
  • 프로젝트
  • 글
  • 링크

© 2026 newgirok

← 글 목록

에러 처리 — 예외가 발생했을 때 무너지지 않는 코드

2025년 9월 6일
JavaScript에러 처리try-catchPromiseasync/await

API를 호출했는데 갑자기 서버가 응답하지 않거나, 사용자가 예상치 못한 값을 입력하는 순간 — 에러는 항상 예고 없이 찾아옵니다. 잡지 않은 에러는 프로그램 전체를 멈춰버립니다.

JavaScript 에러 타입

JavaScript는 상황에 따라 다른 타입의 에러를 던집니다. 어떤 에러인지 알면 원인을 빠르게 파악할 수 있습니다.

// TypeError: 잘못된 타입 또는 null/undefined에 접근
const user = null;
user.name; // TypeError: Cannot read properties of null

// ReferenceError: 선언되지 않은 변수 참조
console.log(notDeclared); // ReferenceError: notDeclared is not defined

// SyntaxError: 문법 오류 (파싱 단계에서 발생)
eval("if ("); // SyntaxError: Unexpected end of input

// RangeError: 허용 범위를 벗어난 값
new Array(-1); // RangeError: Invalid array length

try / catch / finally

try 블록 안에서 에러가 발생하면 즉시 catch로 이동합니다. finally는 에러 발생 여부와 관계없이 항상 실행됩니다.

function readConfig(input) {
  try {
    const config = JSON.parse(input);
    return config;
  } catch (err) {
    console.error("설정 파싱 실패:", err.message);
    return null;
  } finally {
    console.log("파싱 시도 완료"); // 항상 실행
  }
}

readConfig('{ "port": 3000 }'); // 성공 → finally 실행
readConfig("invalid json");      // 실패 → catch → finally 실행

finally는 파일 핸들이나 DB 연결처럼 반드시 정리해야 하는 리소스를 닫는 데 적합합니다.

에러 전파

throw된 에러는 콜 스택을 타고 위로 올라갑니다. 중간에 catch가 없으면 최상위까지 전파됩니다.

         main()
           |
        loadUser()
           |
       fetchData()   ← 여기서 에러 throw
           |
       (에러 전파 위로)
           |
        loadUser()   ← catch 없음, 계속 전파
           |
         main()      ← catch 있으면 여기서 처리
function fetchData() {
  throw new Error("네트워크 오류");
}

function loadUser() {
  fetchData(); // catch 없음 → 에러가 위로 전파
}

function main() {
  try {
    loadUser();
  } catch (err) {
    console.error("main에서 처리:", err.message); // "네트워크 오류"
  }
}

에러 전파(Error Propagation): throw된 에러가 처리될 때까지 호출 스택을 역순으로 올라가는 동작

커스텀 에러 클래스

Error를 상속하면 도메인에 맞는 에러를 정의할 수 있습니다. name, message 외에 code 같은 필드를 추가하면 에러를 더 구체적으로 구분할 수 있습니다.

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.name = "NotFoundError";
  }
}

function getUser(id) {
  if (!id) throw new NotFoundError("사용자");
}

try {
  getUser(null);
} catch (err) {
  if (err instanceof NotFoundError) {
    console.error(`[${err.code}] ${err.message}`); // [NOT_FOUND] 사용자를 찾을 수 없습니다.
  }
}

커스텀 에러: 내장 Error 클래스를 상속해 도메인 의미를 담은 에러 타입을 직접 정의하는 패턴

동기 코드 에러 처리

동기 코드는 try/catch로 충분합니다.

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

try {
  const result = divide(10, 0);
} catch (err) {
  console.error(err.message); // "0으로 나눌 수 없습니다."
}

Promise 에러 처리

Promise에서 발생한 에러는 .catch로 처리합니다. .catch를 붙이지 않으면 unhandledRejection 이벤트가 발생하고, Node.js에서는 프로세스가 종료될 수 있습니다.

function fetchUser(id) {
  return fetch(`/api/users/${id}`)
    .then((res) => {
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      return res.json();
    });
}

fetchUser(1)
  .then((user) => console.log(user))
  .catch((err) => console.error("요청 실패:", err.message));

// .catch() 없이 rejected Promise를 방치하면:
// UnhandledPromiseRejectionWarning (Node.js)
// unhandledrejection 이벤트 (브라우저)

unhandledRejection: catch되지 않은 Promise rejection이 발생했을 때 런타임이 발생시키는 이벤트

async/await 에러 처리

async/await에서 발생한 에러는 동기 코드처럼 try/catch로 잡습니다. await가 rejected Promise를 반환하면 해당 위치에서 에러가 throw됩니다.

async function loadUser(id) {
  try {
    const res = await fetch(`/api/users/${id}`);
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    const user = await res.json();
    return user;
  } catch (err) {
    console.error("사용자 로딩 실패:", err.message);
    throw err; // 필요하다면 상위로 재전파
  }
}

async function main() {
  try {
    const user = await loadUser(1);
    console.log(user);
  } catch (err) {
    console.error("main에서 최종 처리:", err.message);
  }
}

catch 블록에서 throw err를 하지 않으면 에러가 그 자리에서 소멸하고, main은 에러 없이 실행됩니다. 의도적으로 에러를 삼키는 게 아니라면 재전파하거나 명확히 처리해야 합니다.

자주 하는 실수

에러를 무음으로 삼키는 패턴

// 나쁜 예 — 에러가 무슨 이유로 발생했는지 알 수 없음
try {
  doSomething();
} catch (err) {
  // 아무것도 안 함
}

// 좋은 예 — 최소한 로깅
try {
  doSomething();
} catch (err) {
  console.error("doSomething 실패:", err);
}

비동기 에러를 동기 try/catch로 잡으려는 경우

// 잘못된 예 — callback 안에서 throw해도 바깥 catch에 걸리지 않음
try {
  setTimeout(() => {
    throw new Error("타임아웃 에러"); // 잡히지 않음
  }, 1000);
} catch (err) {
  console.error(err); // 실행되지 않음
}

// 올바른 예 — Promise로 감싸거나 콜백 안에서 직접 처리
function delay(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

async function run() {
  try {
    await delay(1000);
    throw new Error("지연 후 에러");
  } catch (err) {
    console.error("잡힘:", err.message);
  }
}

에러 처리 정리

상황처리 방법주의 사항
동기 코드try / catch / finallycatch 안에서 에러를 삼키지 말 것
Promise.catch 체이닝.catch 누락 시 unhandledRejection 발생
async/awaittry / catch 감싸기재전파 여부를 명시적으로 결정할 것
setTimeout/콜백콜백 내부에서 직접 처리바깥 try/catch로는 잡히지 않음

에러는 막을 수 없지만, 어디서 어떻게 처리할지는 선택할 수 있습니다. 에러 전파 흐름을 이해하고 계층별로 책임을 나누면, 예외가 발생해도 무너지지 않는 코드를 작성할 수 있습니다.

← 이전 글비동기 프로그래밍 — JavaScript가 기다리는 방법
다음 글 →상태와 불변성 — 예측 가능한 변경을 만드는 방법