본문 바로가기
4주차

에러 처리의 필요성과 종류

by 두밥비 2025. 5. 9.

안녕하세요. 웹 YB 윤소은입니다! 이제 과제에서 API 연동도 하고, 합동 세미나도 진행하면서 에러 처리에 대해 다시 정리해 보고 싶어서 '에러 처리의 필요성과 종류'를 주제로 선정하게 되었습니다.

사실 에러가 발생하지 않도록 코드를 작성하는 건 거의 불가능합니다.. 그렇기에 에러는 언제 어디에서나 발생할 수 있습니다. 따라서 에러 발생을 막는 것도 중요하지만, 에러가 발생했을 때도 프로그램이 제대로 동작할 수 있도록 에러 처리를 명확하게 해주는 것이 중요합니다.

 

Q. 왜 에러 처리가 필요한가?

 

자바스크립트 프로그램을 실행하는 중, 예기치 않은 문제가 발생할 수 있습니다. 에러가 발생하면 프로그램이 강제 종료될 수 있고, 사용자는 "무슨 일이 일어난 건지" 알 수 없습니다. 또, 디버깅이 어렵고 사용자 경험도 나빠집니다. 그러나 에러 처리를 제대로 해주면, 에러가 발생해도 프로그램이 중단되지 않고 계속 실행됩니다. 나아가 사용자가 이해할 수 있는 적절한 메시지대체 동작을 제공할 수 있습니다. 또, 예외 상황을 제어하고 안정적인 프로그램을 만들 수 있습니다.

 

참고: 에러’와 ‘예외 상황’은 다르다!

예외적인 상황은 직접적인 에러를 발생시키지는 않지만, 예외 상황을 방치하면 결국 에러로 이어질 수 있습니다.
  • 에러: 실제로 오류가 발생한 상황 (ex. undefined에 접근)
  • 예외 상황: 아직 오류는 아니지만 잠재적 오류 상황 (ex. API 응답이 null일 수 있음)
// 예외 상황을 방치하면 에러로 이어질 수 있다
const user = null;
console.log(user.name); // TypeError: Cannot read properties of null

 

 

에러 처리 문법

  1. 예외적인 상황이 발생하면 반환하는 값(null 또는 -1)을 if 문이나 단축 평가 또는 옵셔널 체이닝 연산자를 통해 확인해서 처리하는 방법
  2. 에러 처리 코드를 미리 등록해 두고 에러가 발생하면 에러 처리 코드로 점프하도록 하는 방법
  3. try...catch...finally

 

if 문이나 단축 평가 또는 옵셔널 체이닝 연산자

함수나 API 호출 결과가 null, undefined, -1  특정 값을 반환하는 경우, 이를 통해 직접 에러를 감지하고 제어하는 방식입니다. 이 방법은 명시적인 예외 처리를 피하고, 로직 흐름 안에서 자연스럽게 예외를 처리할 수 있다는 장점이 있습니다.

 

예시

 

(1) null 또는 undefined를 통한 확인

function findUser(id) {
  const users = [{ id: 1, name: 'Alice' }];
  return users.find(user => user.id === id); // 못 찾으면 undefined 반환
}

const user = findUser(2);

if (user) {
  console.log(user.name);
} else {
  console.log("사용자를 찾을 수 없습니다.");
}

 

(2) -1 반환 예시 (indexOf)

const fruits = ["apple", "banana", "mango"];
if (fruits.indexOf("grape") === -1) {
  console.log("해당 과일은 목록에 없습니다.");
}

 

(3) 단축 평가 / 옵셔널 체이닝

const user = null;
console.log(user && user.name);      // undefined
console.log(user?.name);            // undefined (에러 없이 안전)

 

장점

  • 코드 흐름이 직관적이고 try...catch보다 간단한 경우 많음
  • 성능상 부담 없음 (예외 발생 없이 처리됨)
  • API 호출 후 상태 코드 확인 같은 작업에 적합

단점

  • 예외 상황의 원인을 추적하기 어려움
  • 반환값을 제대로 검사하지 않으면 침묵하는 에러가 발생할 수 있음
  • 로직이 커질수록 예외 처리가 누락될 위험이 있음

 

try...catch...finally

 

자바스크립트에서는 try...catch 문을 사용해 에러를 처리합니다.

try...catch...finally 문은 3개의 코드 블록으로 구성됩니다. finally 문은 불필요하다면 생략이 가능합니다. catch 문도 생략 가능하지만 catch 문이 없는 try 문은 의미가 없기 때문에 생략하지 않습니다.

try {
  // 에러가 발생할 수 있는 코드
} catch (error) {
  // 에러가 발생했을 때 실행할 코드
  // error에는 try 코드 블록에서 발생한 Error 객체가 전달
} finally {
  // 에러 발생 여부와 관계없이 무조건 실행 (선택)
}

 

try...catch...finally 문을 실행하면 먼저 try 코드 블록이 실행됩니다. 이때 try 코드 블록에 포함된 문 중에서 에러가 발생하면 발생한 에러는 catch 문의 error 변수에 전달되고 catch 코드 블록이 실행됩니다. catch 문의 error 변수는 try 코드 블록에 포함된 문 중에서 에러가 발생하면 생성되고 catch 코드 블록에서만 유효합니다. finally 코드 블록은 에러 발생과 상관없이 반드시 한 번 실행됩니다. try...catch...finally 문으로 에러를 처리하면 프로그램이 강제로 종료되지 않습니다!

 

예제: 기본 구조

try {
  const result = someFunction(); // 에러 발생 가능성 있음
  console.log(result);
} catch (err) {
  console.error('에러 발생:', err.message); // 사용자에게 친절한 메시지 제공
} finally {
  console.log('항상 실행되는 코드 (리소스 정리 등)');
}

 

 

Error 생성자 함수

 

자바스크립트는 Error 생성자 함수를 포함해 7가지의 에러 객체를 생성할 수 있는 Error 생성자 함수를 제공합니다.

자바스크립트는 에러 정보를 담기 위한 Error 객체를 제공합니다.

 

기본 제공되는 에러 생성자 종류

생성자 함수 인스턴스
Error 모든 에러의 기본 타입
ReferenceError 참조할 수 없는 변수 사용 시 발생
TypeError 잘못된 타입의 값 사용 시 발생
SyntaxError 문법적으로 잘못된 코드일 때
RangeError 숫자 범위가 잘못됐을 때
EvalError eval 함수 관련 에러 (거의 사용 안 됨)
URIError 잘못된 URI 처리 시

 

위의 생성자 함수들이 생성한 에러 객체의 프로토타입은 모두 Error.prototype을 상속받습니다.

 

const error = new Error('문제가 발생했습니다!');
console.log(error.name); // "Error"
console.log(error.message); // "문제가 발생했습니다!"
console.log(error.stack); // 스택 트레이스 출력

 

예제

try {
  undefinedFunction();
} catch (e) {
  console.error(e instanceof ReferenceError); // true
  console.error(e.message); // undefinedFunction is not defined
}

throw 문 - 에러 발생시키기

단순히 Error 객체를 생성한다고 자동으로 에러가 발생하지는 않습니다.
에러를 던지는 역할은 throw 문이 합니다.

 

자바스크립트에서 throw 문은 개발자가 명시적으로 에러를 발생시킬 수 있도록 해주는 문법입니다.

우리는 코드에서 잘못된 상황(예: 0으로 나누기, 유효하지 않은 입력 등)을 감지했을 때, 개발자가 직접 그 상황을 예외로 다루고 싶을 수 있습니다. 이럴 때 throw를 사용하여 의도적으로 프로그램 흐름을 중단시키고, 에러 처리 로직(catch)로 제어를 넘기게 만들 수 있습니다.

 

Error 객체 생성만으로는 에러가 발생하지 않는다.

const error = new Error('에러가 발생했습니다!');
console.log(error); // 단지 Error 객체일 뿐, 실제로 에러는 발생하지 않음

 

위의 예제에서 Error 생성자 함수는 단지 에러 정보를 담는 객체를 생성할 뿐이고 프로그램의 흐름에는 영향을 주지 않습니다.

 

👉 반드시 throw로 던져야 에러로 인식되고 중단됩니다

throw new Error('에러가 발생했습니다!'); // 여기서 진짜로 에러 발생

 

 

 

기본 사용 예제

function divide(a, b) {
  if (b === 0) {
    throw new Error("0으로 나눌 수 없습니다."); // 에러 객체를 던짐
  }
  return a / b;
}

try {
  console.log(divide(10, 0));
} catch (e) {
  console.error("에러 메시지:", e.message); // "0으로 나눌 수 없습니다."
}

 

실행 흐름 설명

  1. divide(10, 0) 호출
  2. b === 0이므로 조건 충족
  3. throw 문이 실행되며 Error 객체가 던져짐
  4. catch 블록으로 제어가 이동
  5. 프로그램은 강제 종료되지 않고, 에러 메시지를 출력한 뒤 정상적으로 종료됨

 

throw 뒤에는 어떤 값이든 올 수 있다?

자바스크립트에서는 throw 뒤에 모든 표현식(값)이 올 수 있습니다.

throw '문자열 에러';           // 문자열도 가능 (권장하지 않음)
throw 42;                     // 숫자도 가능 (권장하지 않음)
throw true;                   // 불리언도 가능 (권장하지 않음)
throw { message: '에러' };     // 객체도 가능
하지만 일반적으로는 Error 객체 또는 그 하위 클래스를 사용합니다.

 

왜 Error 객체를 던지는 것이 더 좋은가?

1) 표준화된 프로퍼티 제공

Error 객체가 제공하는 프로퍼티

name 에러의 이름 (기본값은 "Error")
message 에러 메시지
stack 에러 발생 위치를 추적할 수 있는 스택 트레이스

 

2) 디버깅, 로깅, 모니터링 도구와의 호환성

많은 디버깅 도구나 로깅 시스템은 Error 객체를 기준으로 분석하므로, throw할 때 문자열이나 숫자보다 Error 객체를 사용하는 것이 유지보수에 유리합니다.

 

다양한 커스텀 에러 예시

Bad) 문자열 에러 던지기

try {
  throw "이건 문자열 에러입니다";
} catch (e) {
  console.log(typeof e); // string
  console.log(e);        // 이건 문자열 에러입니다
}
  • 에러의 종류나 스택 추적이 안됨
  • 유지보수에 좋지 않음

 

Good) Error 객체 던지기

try {
  throw new Error("이건 Error 객체입니다");
} catch (e) {
  console.log(e instanceof Error); // true
  console.log(e.name);             // "Error"
  console.log(e.message);          // "이건 Error 객체입니다"
  console.log(e.stack);            // 에러 발생 위치 정보 포함
}

 

 

사용자 정의 에러 만들기 (Custom Error)

자바스크립트에서는 Error 클래스를 상속하여 나만의 에러 클래스를 만들 수 있습니다. 이를 사용자 정의 에러라고 하며, 특정한 예외 상황을 의미 있게 분리하여 처리할 수 있도록 도와줍니다.
class ValidationError extends Error {
  constructor(message) {
    super(message);
    this.name = "ValidationError";
  }
}

function validateEmail(email) {
  if (!email.includes("@")) {
    throw new ValidationError("유효하지 않은 이메일 형식입니다.");
  }
  return true;
}

try {
  validateEmail("hello-world");
} catch (e) {
  console.error(e.name);    // ValidationError
  console.error(e.message); // 유효하지 않은 이메일 형식입니다.
}

 

코드 설명

1. 사용자 정의 에러 클래스 정의

class ValidationError extends Error {
  constructor(message) {
    super(message); // Error 생성자에 에러 메시지 전달
    this.name = "ValidationError"; // 에러 이름 명시
  }
}
  • ValidationError는 기본 Error 클래스를 상속합니다.
  • 생성자에서 super(message)를 호출하여 기본 에러 메시지 시스템을 그대로 사용합니다.
  • 기본 Error의 name 프로퍼티는 "Error"이지만, 이를 "ValidationError"로 오버라이드하여 구분이 가능하게 만듭니다.
  • 나중에 여러 종류의 에러를 처리할 때 "에러 타입에 따라 분기 처리"가 가능합니다.

 

2. 이메일 유효성 검사 함수

function validateEmail(email) {
  if (!email.includes("@")) {
    throw new ValidationError("유효하지 않은 이메일 형식입니다.");
  }
  return true;
}
  • 입력받은 이메일 문자열에 @ 문자가 포함되어 있지 않으면, 유효하지 않은 이메일로 간주합니다.
  • 이때, 기본 Error가 아닌 ValidationError를 던짐으로써 "검증 실패"에 해당하는 명확한 의미를 부여합니다.
  • 유효한 경우는 true를 반환하여 검증 통과를 나타냅니다.

 

3. try...catch로 에러 처리

try {
  validateEmail("hello-world"); // 잘못된 이메일 포맷
} catch (e) {
  console.error(e.name);    // "ValidationError"
  console.error(e.message); // "유효하지 않은 이메일 형식입니다."
}
  • validateEmail 함수는 잘못된 이메일을 인자로 받아 에러를 발생시킵니다.
  • 이 에러는 try...catch 문으로 감싸져 있기 때문에 프로그램은 강제 종료되지 않고, catch 블록에서 에러 객체의 정보(name, message)를 출력합니다.

 

왜 사용자 정의 에러를 써야 할까?

기본 Error만 사용할 경우 사용자 정의 Error를 사용할 경우
에러의 종류를 구분하기 어려움 에러 타입에 따라 구체적인 처리 가능
모든 에러가 "Error"로 나타남 "ValidationError", "AuthError" 등 구체적 이름 사용 가능
협업/디버깅 시 혼란 가능 코드 가독성 및 유지보수 용이

 


에러의 전파 (Propagation)

에러는 함수 호출 스택을 따라 아래 방향으로 전파됩니다. 즉, 에러가 발생한 함수의 호출자에게 전달되며, 최종적으로 try...catch가 없는 경우 프로그램은 종료됩니다.

 

예제

const foo = () => {
  throw new Error("foo에서 에러 발생");
};

const bar = () => {
  foo(); // 에러가 여기로 전파됨
};

const baz = () => {
  bar(); // 에러가 여기로 전파됨
};

try {
  baz(); // 최종 호출 지점에서 try...catch로 감싸줌
} catch (e) {
  console.error("잡은 에러:", e.message);
}
에러를 어딘가에서 반드시 catch 해야 프로그램이 종료되지 않습니다.

비동기 코드에서 주의할 점

비동기 코드(ex. setTimeout, Promise, fetch)는 일반적인 try...catch로 에러를 잡을 수 없습니다.

 

작동하지 않는 예시

try {
  setTimeout(() => {
    throw new Error("이 에러는 못 잡음");
  }, 1000);
} catch (e) {
  console.error("못 잡힘:", e.message);
}

위의 throw는 비동기 콜백 내부에서 발생하기 때문에, try...catch가 감싸고 있어도 무용지물입니다.

 

해결 방법

  1. 비동기 내부에서 자체적으로 try...catch 사용
  2. Promise에서는 .catch() 또는 async/await + try...catch 사용
// 1. 비동기 내부에서 처리
setTimeout(() => {
  try {
    throw new Error("에러 발생");
  } catch (e) {
    console.error("비동기 내부에서 캐치됨:", e.message);
  }
}, 1000);

// 2. async/await로 처리
const asyncFunc = async () => {
  try {
    const res = await fetch('https://wrong.url');
    const data = await res.json();
  } catch (e) {
    console.error('비동기 에러 캐치:', e.message);
  }
};

asyncFunc();

결론

에러는 언제, 어디서든 발생할 수 있는 것임을 항상 염두에 두어야 합니다. 코드를 작성할 때는 정상적인 흐름만을 가정하고 개발해서는 안 되며, 예기치 못한 상황이나 사용자 입력 오류, 네트워크 문제 등 다양한 예외 상황을 고려한 철저한 방어적 코딩이 필요합니다.

 

에러가 발생했을 때는 사용자에게 친절하고 명확한 에러 메시지를 제공함으로써, 문제의 원인을 이해하고 적절한 행동을 취할 수 있도록 안내하는 것이 중요합니다. 하지만 동시에, 개발 내부 로직이나 민감한 시스템 정보를 노출하지 않도록 주의해야 하며, 이는 보안 및 사용자 신뢰 측면에서도 매우 중요한 부분입니다!!

 

또 최근 웹 환경에서는 비동기 처리가 자주 사용되기 때문에 동기 흐름뿐 아니라 비동기 흐름에서 발생할 수 있는 에러까지 고려한 에러 핸들링 로직을 설계하는 습관이 필요합니다. 이를 위해서는 try...catch를 적절히 활용하고, Promise, async/await와 같은 비동기 구조에서의 에러 전파 방식과 처리 방법에 대한 정확한 이해가 바탕이 되어야 합니다.

따라서, 에러 처리는 단순히 프로그램의 예외를 막는 역할이 아니라, 사용자 경험을 지키고 시스템의 신뢰성과 안정성을 높이기 위한 핵심 개발 전략 중 하나라는 것을 기억해 주시면 좋겠습니다!

 

참고 자료

 

모던 자바스크립트 Deep Dive - 예스24

『모던 자바스크립트 Deep Dive』에서는 자바스크립트를 둘러싼 기본 개념을 정확하고 구체적으로 설명하고, 자바스크립트 코드의 동작 원리를 집요하게 파헤친다. 따라서 여러분이 작성한 코드

www.yes24.com

 

try...catch - JavaScript | MDN

The try...catch statement is comprised of a try block and either a catch block, a finally block, or both. The code in the try block is executed first, and if it throws an exception, the code in the catch block will be executed. The code in the finally bloc

developer.mozilla.org