try...catch, 자바스크립트에서의 예외 처리

패스트 캠퍼스 프론트엔드 개발 스쿨에서 배운 내용을 정리합니다.
내용에 오류가 있다면 댓글 남겨주시기 바랍니다.

예외처리

프로그램의 언어들이 항상 완벽하게 동작하지는 않는다.
단순 버그이거나, 프로그래머가 잘못 작성했거나, 프로그래밍 언어 자체가 에러를 내는 경우가 있다.

1. 동기식 코드에서의 예외 처리

프론트엔드 개발자의 실수

1
new Array(-1) // RangeError: Invalid array length

1
console.log(foo) // ReferenceError: foo is not defined

프론트엔드 개발자의 의도와 다른 실수

1
2
fetch('https://nonexistent-domain.nowhere'); // TypeError: Failed to fetch
// 위 주소는 맞는데 서버/네트워크 쪽 에러가 있다거나

에러가 발생하면 나머지 로직이 실행되지 않는다. 그 시점에 실행 중이었던 작업을 완료할 수 없게 된다.

1
2
3
console.log('에러가 나기 직전까지의 코드는 잘 실행됩니다.');
new Array(-1); // RangeError: Invalid array length
console.log('에러가 난 이후의 코드는 실행되지 않습니다.'); // 이것은 실행되지 않는다.

위와 같이 코드의 실행이 중단된다.

try...catch...finally 구문을 사용하면 에러가 나더라도 코드의 실행을 지속할 수 있다.

1
2
3
4
5
6
7
8
9
10
try {
// 여기서 에러가 나면 에러가 난 시점에 코드의 흐름이
// catch로 넘어간다.
console.log('에러가 나기 직전까지의 코드는 잘 실행됩니다.');
new Array(-1); // RangeError: Invalid array length
console.log('에러가 난 이후의 코드는 실행되지 않습니다.');
} catch (e) {
console.log('코드의 실행 흐름이 catch 블록으로 옮겨집니다.');
alert(`다음과 같은 에러가 발생했습니다: ${e.name}: ${e.message}`);
}
  • try: 에러가 났을 때 원상복구를 시도할 코드. 에러 발생시 코드의 실행 흐름이 catch 블록으로 옮겨간다.
  • catch: 에러에 대한 정보를 담고 있는 객체(위 예제의 e)를 사용할 수 있다.

    • e.name: RangeError같은 에러의 이름
    • e.meassage: Invalid array length 에러 메시지
    • 1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      try {
      console.log('에러가 나기 직전까지의 코드는 잘 실행됩니다.');
      /* new Array(-1); */
      console.log(foo);
      console.log('에러가 난 이후의 코드는 실행되지 않습니다.');
      } catch (e) {
      if(e.name === 'RangeError') {
      alert(`배열 생성자에 잘못된 인수가 입력되었습니다.`)
      } else if (e.name === `ReferenceError`) {
      alert(`선언되지 않은 변수가 사용되고 있습니다.`)
      }
      console.log('코드의 실행 흐름이 catch 블록으로 옮겨집니다.');
      }
  • finally: try블록 안에서의 에러 발생 여부와 관계 없이 무조건 실행되어야 하는 코드
    return, break, continue등으로 코드의 실행 흐름이 즉시 이동되더라도 실행된다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    for (let i of [1, 2, 3]) {
    try {
    if (i === 3) {
    break;
    }
    } finally {
    console.log(`현재 i의 값: ${i}`);
    }
    }
    • finally블록은 catch 블록과도 같이 사용된다.
      • 에러가 안 났을 때: try - finally
      • 에러가 났을 때: try - 에러발생 - catch - finally

1-1. 직접 에러 발생시키기

코드를 다른 사람이나 미래의 내가 의도한 대로 사용하지 않을 경우 에러가 발생하도록 할 수 있다.

Error생성자, throw구문

1
2
3
4
5
6
const even = parseInt(prompt('짝수를 입력하세요'));
if (even % 2 !== 0) {
throw new Error('짝수가 아닙니다.');
}
// 3을 입력할 경우
// Error: 짝수가 아닙니다.

1
2
3
4
5
6
7
8
9
throw 'ggg';
// 던지는 것에 제한이 없으나
throw new Error('ddd')
// 반드시 에러 객체를 던져야 한다.

const e = new Error('짝수가 아니다')
// Error는 어디서나 쓸 수 있는 객체(생성자)이다.
e;
// Error: 짝수가 아니다.

다음과 같이 내가 에러를 만들어 던질 수 있고, 이를 try...catch 구문으로 잡을 수 있다.

1
2
3
4
5
6
7
8
try {
const even = parseInt(prompt('짝수를 입력하세요'));
if (even % 2 !== 0) {
throw new Error('짝수가 아닙니다.');
}
} catch (e) {
alert(e.message);
}

복잡한 프로그램을 짜다보면 추가적인 자세한 정보를 추가해서 에러를 만들고 싶게 된다.
에러의 종류를 구분해야 하거나 에러 객체에 기능을 추가해야 할 필요가 있다.

그냥 내장 에러 생성자가 아니라 내가 자체 Error 클래스를 만들경우 다음과 같은 방법이 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MyError extends Error {
constructor(value, ...params) {
super(...params);
this.value = value;
this.name = 'MyError';
}
}

try {
const even = parseInt(prompt('짝수를 입력하세요'));
if (even % 2 !== 0) {
throw new MyError(even, '짝수가 아닙니다.');
}
} catch (e) {
if (e instanceof MyError) {
console.log(`${e.name}: ${e.value}${e.message}`);
}
}

에러에서도 동기식 처리와 비동기식 처리가 있다.

2. 비동기식 코드에서의 예외 처리

2-1. 비동기 콜백

비동기식으로 작동하는 콜백의 내부에서 발생한 에러는, 콜백 바깥에 있는 try 블록으로는 잡아낼 수 없다.

1
2
3
4
5
6
7
8
9
10
try {
setTimeout(() => {
// 비동기 코드 안의 에러는 잡히지 않는다.
throw new Error('에러!');
});
} catch (e) {
console.error(e);
}
// Uncaught Error: 에러!
// 에러가 잡히지 않아서 생기는 에러;;

JavaScript 엔진은 에러가 발생하는 순간 호출 스택을 되감는 과정을 거친다.
이 과정 중에 try 블록을 만나야 코드의 실행 흐름을 원상복구시킬 수 있다.

에러는 호출스택과 관련되어 있다.

아래는 동기식 예외 발생

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function add(x, y) {
// 에러를 이곳에서 발생시키고
new Array(-1);
return x + y;
}

function add2(x) {
return add(x, 2);
}

function add2AndPrint(x) {
// try... catch 구문이 여기있지만
// 자바스크립트 실행 엔진이 에러가 발생하면 호출 스택을 하나씩 지워가면서
// try가 있는지 찾아본다.
try {
// 여기서 add2를 호출하고 add2는 add를 호출하기 때문에 에러가 잡힌다.
const result = add2(x);
console.log(result);
} catch(e) {
alert('잡았다!');
}
}

add2AndPrint(3); // alert으로 '잡았다!'가 뜬다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function add(x, y) {
// 에러를 이곳에서 발생시키고
new Array(-1);
return x + y;
}

function add2(x) {
// 호출스택을 되감다 가장 먼저 만나는 try에서 에러가 잡힌다.
try {
return add(x, 2);
} catch(e) {
alert('add2에서 잡히나?')
}
}

function add2AndPrint(x) {
try {
const result = add2(x);
console.log(result);
} catch(e) {
alert('잡았다!');
}
}

add2AndPrint(3); // alert으로 'add2에서 잡히나?'가 뜬다.

비동기 콜백이라면 단순 try...catch 구문으로는 잡을 수 없다.
비동기 콜백이 실행될 때는 이미 호출스택이 비워져 try를 찾을 수 없다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function add(x, y) {
setTimeout(() => {
// 이 콜백이 일단 태스크큐에 들어갔다가 호출 스택이 비워지면 실행된다.
// 호출스택을 되감아 가면서 try를 찾아야하는데
// 이 콜백이 실행될 때는 이미 호출스택이 비워졌으므로 try를 찾을 수 없다.
new Array(-1);
})
return x + y;
}

function add2(x) {
try {
return add(x, 2);
} catch(e) {
alert('add2에서 잡히나?')
}
}

function add2AndPrint(x) {
try {
const result = add2(x);
console.log(result);
} catch(e) {
alert('잡았다!');
}
}

add2AndPrint(3);

따라서, 비동기 콜백 내부에 try를 작성해주어야 한다.

이벤트 리스너와 try catch 블록 예제

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const buttonEl = document.querySelector('button');

try {
buttonEl.addEventListener('click', e => {
try {
new Array(-1);
alert('버튼이 눌렸습니다.');
} catch (e) {
alert('이벤트 리스너 안에서 에러가 발생했습니다.');
}
})
} catch (e) {
alert('에러가 발생했습니다.'); // 출력되지 않음
}

2-2. Pomise

Promise 객체는 세 가지 상태를 가질 수 있다.

  • pending - Promise 객체에 결과값이 채워지지 않은 상태
  • fulfilled - Promise 객체에 결과값이 채워진 상태(이때 then메소드 또는 await를 통해 무언가를 실행했다.)
  • rejected - Promise 객체에 결과값을 채우려고 시도하다가 에러가 난 상태
    • then메소드에 첫 번째 인수로 넘겨준 콜백이 실행되지 않고, 두 번째 인수로 넘겨준 콜백이 실행된다. 그리고 이 콜백에는 에러 객체가 첫번째 인수로 주어진다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const p = new Promise(resolve => {
const even = parseInt(prompt('짝수를 입력하세요'));
if (even % 2 !== 0) {
throw new Error('짝수가 아닙니다.');
} else {
// 짝수면 fullfiled되어 then메소드의 첫번째 인수로 들어간 함수가 실행
resolve(even);
}
});

// then 콜백에서 반환된 값이 다음Promise의 값이 된다.
p.then(even => {
return '짝수입니다.';
}, e => {
return e.message;
}).then(alert);
// .then(msg => alert(msg))와 같음

Promise가 rejected 상태가 되었을 때 catch 메소드를 통해 다음과 같은 방법으로도 에러 처리 콜백을 지정해 줄 수 있다.

1
2
3
4
5
p.then(even => {
return '짝수입니다.';
}).catch(e => {
return e.message;
}).then(alert);

2-3. 비동기 함수

비동기 코드에서의 try...catch와 비동기 함수에서의 try...catch는 다르게 동작한다. (내부 동작 방식이 완전히 다르다. - 비동기 함수를 사용하면 예외처리도 보다 편하게 할 수 있다.)

비동기 함수 내부에서는, rejected 상태가 된 Promise객체를 동기식 예외처리 방식과 동일하게 try...catch...finally 구문으로 처리할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
async function func() {
try {
// ※ 단, Promise 객체에 대해 await 구문을 사용해야만
// 에러가 발생했을 때 catch 블록으로 코드의 실행 흐름이 이동한다.
const res = await fetch('https://nonexistent-domain.nowhere');
} catch (e) {
console.log(e.message);
}
}

func(); // 출력 결과: Failed to fetch

Share Comments