개발자가 작성하는 모듈에서 발생가능한 에러 상황에서 던지게 되며 상위 계층이나 호출하는 곳에서 모듈의 에러를 감지할 수 있다. 아래는 에러를 던지는 기본적인 방법이다.
// func.js
function someFunc(someParam) {
if (!someParam) {
throw new Error('someError');
}
// ...someFunc의 로직
return someParam;
}
module.exports = { someFunc }
위 예시 코드에서 매개 변수 someParam이 특정한 값을 가지고 있지 않다면 에러를 던진다. someFunc이 someParam 값이 없을 때, 더 이상 함수를 진행할 필요가 없을 때 처리할 때 사용된다.
// func.js
...someFunc
async function someAsyncFunc(someParam) {
if (!someParam) {
throw new Error('someError');
}
// ...someAsyncFunc의 로직
return someParam;
}
module.exports = { someFunc, someAsyncFunc }
위 내용은 비동기 함수 내에서 throw 하는 코드이다. 에러를 던질 때에는 위 동기방식의 함수와 큰 차이는 없다. 다만, 비동기 함수의 throw는 Promise Rejection을 발생시키기 때문에 에러를 잡아내는 곳에서는 다른 방식을 이용해야한다.
그리고 비동기 작업은 동기적인 에러 핸들링 방식으로 처리 할 수 없다. 만일 비동기 에러를 처리하지 않으면 unhandled promise rejection
으로 인해 프로그램 자체를 종료시키기 때문에 꼭 비동기적인 방법을 이용하여서 해당 에러를 사전에 핸들링 해주어야 한다.
이를 위해서 활용가능한 두 가지 방법은 바로 (1) await 를 사용하여 try - catch 로 에러 핸들링을 하는 것과 (2) promise - catch의 기능을 이용하여 에러 핸들링을 하는 방법이 있습니다. Express 에서는 async wrapping 모듈로 비동기 함수를 처리할 수 있다.
라이브러리 혹은 개발자가 작성한 모듈에서 throw가 발생했다면 상위 모듈에서 해당 에러를 잡아낼 수 있다. 에러를 잡아낸 후 return 및 throw를 하지 않으면 로직은 계속 진행되며 멈추지 않는다.
// caller.js
const { someFunc } = require('./func');
function caller() {
const someValueWithParam = someFunc(1);
console.log("someValue:", someValue1);
// someValue: 1
const someValueWithoutParam = someFunc();
// Error: someError
// 에러가 발생하였으므로 더 이상 실행되지 않는다.
console.log('someValue', someValueWithoutParam);
}
caller();
// 최종적으로 콘솔에 보이는 것
someValue: 1
위 예시 코드는 someFunc을 import 하여 매개 인자에 값을 넣었을 때와 넣지 않았을 때 차이를 보여주는 코드이다. 매개 인자에 값을 넣게 되면 someFunc의 로직에 의해 정상적인 값을 반환해주지만 매개 인자에 값을 넣지 않는다면 someFunc은 throw를 하게 된다. 그 때, 에러가 발생하였기 때문에 더 이상 로직이 실행되지 않고 함수가 종료된다. 이제 try - catch 구문을 사용해보자.
// caller.js
const { someFunc } = require('./func');
function caller() {
const someValueWithParam = someFunc(1);
console.log("someValue:", someValueWithParam);
// someValue: 1
try {
const someValueWithoutParam = someFunc();
// 에러가 발생하였으므로 더 이상 실행되지 않는다.
console.log('someValue', someValueWithoutParam);
}
catch(error) {
console.log(error);
// Error: someError
}
console.log('여기는 실행됩니다.');
}
caller();
// 최종적으로 콘솔에 보이는 것
someValue: 1
Error: someError
여기는 실행됩니다.
위 예시 코드는 try-catch
를 써서 에러가 발생하면 해당 에러를 잡아두도록 프로그래밍 되어 있다. try 안에 에러를 검출해 낼 코드를 작성하고 catch에 에러를 검출했을 시에 관한 작업을 작성한다. 이후 try - catch 문을 제외한 부분은 다시 정상적으로 실행이 됩니다.
try-catch
이외에 try-catch-finally
로도 작성할 수 있다. finally 블록에 있는 구문은 try 혹은 catch 이후에 무조건적으로 한번은 실행되는 영역이다.
// caller.js
const { someAsyncFunc } = require('./func');
function caller() {
try {
someAsyncFunc();
}
catch(error) {
console.log(error);
}
}
caller();
// 최종적으로 콘솔에 보이는 것
Unhandled Promise Rejection: Error: someError
위 예시 코드에서 비동기 함수 에러는 잡히지 않는다.
최종적으로 unhandled 에러 즉, 처리되지 않는 에러로 잡히게 되는데, 그 이유는 이벤트 루프, 태스크 큐와 관련된 내용과 연관되어 있으며 추후 작성할 예정이다. 이 처리되지 않은 에러를 핸들링 하기 위해서 특수한 장치를 두어야 잡히게 되는데 두 가지 방법이 있다. 첫번째는 await
를 사용하는 방법, 두번째는 promise - catch
를 사용하는 방법이다.
// caller.js
const { someAsyncFunc } = require('./func');
async function caller() {
console.log('첫번째 콘솔');
try {
await someAsyncFunc();
}
catch(error) {
console.log(error);
// Error: someError
}
console.log('두번째 콘솔');
}
caller();
// 최종적으로 콘솔에 보이는 것
첫번째 콘솔
Error: someError
두번째 콘솔
await를 사용하면 동기방식에서 사용했던 방법대로 try - catch 구문을 사용할 수 있다. 다만 하위 모듈 { someAsyncFunc } 에 await 를 걸어주기 위해 상위 모듈 ‘caller( )’ 또한 async 함수로 만들어 주어야 하는 현상이 생긴다. 만약 caller() 함수를 비동기로 만들고 싶지 않으면 promise, catch 방식을 사용한다.
// caller.js
const { someAsyncFunc } = require('./func');
function caller() {
console.log('첫번째 콘솔');
someAsyncFunc().catch((error) => {
console.log(error);
// Error: someError
});
console.log('두번째 콘솔');
}
caller();
// 최종적으로 콘솔에 보이는 것
첫번째 콘솔
두번째 콘솔
Error: someError
promise-catch는 위 ‘someAsyncFunc()’ 함수의 리턴값이 그러 하듯이 (async function은 늘 promise 를 리턴한다) promise를 리턴 받는 상황에서 사용할 수 있다.
이는 await 를 사용하지 않아 caller를 동기 함수로 유지할 수 있다. 하지만 비동기 함수는 비동기적으로 로직 처리 및 에러 처리를 하기 때문에 동기적으로 작동하지 않는다. 그래서 위 콘솔과 같이 동기적인 작업들이 먼저 출력이 되고 그 뒤에 비동기 작업들이 출력된다.
Express에서의 에러는 하나의 미들웨어에서 처리할 수 있게끔 만들 수 있다. 개발자는 에러 핸들링 미들웨어를 별도로 두어 자신의 개발환경을 보다 더 최적화할 수 있습니다.
// app.js
const express = require('express');
const { someFunc, someAsyncFunc } = require('./func');
const app = express();
app.get('/someFunc', (req, res) => {
const { someQuery } = req.query;
const someValue = someFunc(someQuery);
res.json({ result: someValue });
});
app.get('/someAsyncFunc', (req, res) => {
const { someQuery } = req.query;
const someValue = someAsyncFunc(someQuery);
res.json({ result: someValue });
});
// error handling 미들웨어
app.use((err, req, res, next) => {
if (err.message === 'someError') {
res.status(400).json({ message: "someQuery notfound." });
return;
}
res.status(500).json({ message: "internal server error" });
});
app.listen(3000);
미들웨어를 추가하여서 이제 라우터에서 던지는 에러를 하나로 통일하여 받을 수 있게 되었다.
위 코드 예시에서 가장 마지막에 적혀있는 error handling 미들웨어
에서 보는 것과 같이 이제 사용자에게 어떤 에러가 갈지 예측할 수 있으며 이는 일관적인 인터페이스를 유지할 수 있게 만들어 준다. 하지만 이 방법으로 했을 경우 여전히 맹점이 있는데, 바로 비동기 모듈 에러는 잡지 못 한다는 점이다. 이는 또 다른 모듈인 async wrapping
을 작성 및 적용하여 해결 할 수 있게 된다.
// async-wrap.js
function asyncWrap(asyncController) {
return async (req, res, next) => {
try {
await asyncController(req, res)
}
catch(error) {
next(error);
}
};
}
module.exports = asyncWrap;
---------------------------------------------------------------
// app.js
const asyncWrap = require('./async-wrap');
app.get('/someAsyncFunc', asyncWrap(async (req, res) => {
const { someQuery } = req.query;
const someValue = await someAsyncFunc(someQuery);
res.json({ result: someValue });
}));
이제 asyncWrap
을 컨트롤러에 씌워 주게 된다면 비동기 컨트롤러에서 생기는 에러를 잡을 수 있게 된다. asyncWrap
은 컨트롤러를 받아서 비동기 에러를 처리하는 새로운 컨트롤러를 만드는 모듈이다. 해당 에러는 ‘next’를 통해 에러 핸들링 미들웨어로 넘어가게 된다.