패스트 캠퍼스 프론트엔드 개발 스쿨에서 배운 내용을 정리합니다.
내용에 오류가 있다면 댓글 남겨주시기 바랍니다.
예외처리
프로그램의 언어들이 항상 완벽하게 동작하지는 않는다.
단순 버그이거나, 프로그래머가 잘못 작성했거나, 프로그래밍 언어 자체가 에러를 내는 경우가 있다.
1. 동기식 코드에서의 예외 처리
프론트엔드 개발자의 실수1
new Array(-1) // RangeError: Invalid array length
1 | console.log(foo) // ReferenceError: foo is not defined |
프론트엔드 개발자의 의도와 다른 실수1
2fetch('https://nonexistent-domain.nowhere'); // TypeError: Failed to fetch
// 위 주소는 맞는데 서버/네트워크 쪽 에러가 있다거나
에러가 발생하면 나머지 로직이 실행되지 않는다. 그 시점에 실행 중이었던 작업을 완료할 수 없게 된다.1
2
3console.log('에러가 나기 직전까지의 코드는 잘 실행됩니다.');
new Array(-1); // RangeError: Invalid array length
console.log('에러가 난 이후의 코드는 실행되지 않습니다.'); // 이것은 실행되지 않는다.
위와 같이 코드의 실행이 중단된다.
try...catch...finally
구문을 사용하면 에러가 나더라도 코드의 실행을 지속할 수 있다.
1 | try { |
try
: 에러가 났을 때 원상복구를 시도할 코드. 에러 발생시 코드의 실행 흐름이catch
블록으로 옮겨간다.catch
: 에러에 대한 정보를 담고 있는 객체(위 예제의e
)를 사용할 수 있다.e.name
:RangeError
같은 에러의 이름e.meassage
:Invalid array length
에러 메시지1
2
3
4
5
6
7
8
9
10
11
12
13try {
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
9for (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
6const even = parseInt(prompt('짝수를 입력하세요'));
if (even % 2 !== 0) {
throw new Error('짝수가 아닙니다.');
}
// 3을 입력할 경우
// Error: 짝수가 아닙니다.
1 | throw 'ggg'; |
다음과 같이 내가 에러를 만들어 던질 수 있고, 이를 try...catch
구문으로 잡을 수 있다.1
2
3
4
5
6
7
8try {
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
18class 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
10try {
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
24function 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 | function add(x, y) { |
비동기 콜백이라면 단순 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
28function 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
14const 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 | const p = new Promise(resolve => { |
Promise
가 rejected 상태가 되었을 때 catch
메소드를 통해 다음과 같은 방법으로도 에러 처리 콜백을 지정해 줄 수 있다.1
2
3
4
5p.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
11async function func() {
try {
// ※ 단, Promise 객체에 대해 await 구문을 사용해야만
// 에러가 발생했을 때 catch 블록으로 코드의 실행 흐름이 이동한다.
const res = await fetch('https://nonexistent-domain.nowhere');
} catch (e) {
console.log(e.message);
}
}
func(); // 출력 결과: Failed to fetch