에러 처리의 모든 것(동기, 비동기)

에러 처리가 중요한 이유

서버에서 에러를 잘 처리하는 것은 엄청 중요하다. 수십 ~ 수만명이 동시다발적으로 접속해서 사용하는 어플리케이션의 서버가 될 수 있기 때문에, 적절히 에러를 처리하지 못했을 경우에 수만명이 서버를 이용하지 못하는 심각한 상황이 생길 수 있다.

에러 처리를 잘한다는게 무슨 말일까 ?

  • 클라이언트가 요청한 request를 제대로 처리하지 못했다면, 적절한 에러 메세지를 보내주어서 클라이언트에게 충분한 에러에 대한 내용을 전달한다.
  • 시스템 내부적으로 큰 문제가 발생하더라도 서버가 중지되지 않도록, 문제 상황에서 빠르게 복구될 수 있도록 예외처리를 잘 하는것

동기 / 비동기 에러 처리

코드 마지막에 최후의 보루로써 에러 핸들러를 두긴 하지만, 어디서 에러가 발생했는지 더 상세히 로깅하고 클라이언트에게 충분한 에러 내용 전달을 위해 각각의 미들웨어에서 에러처리를 하는게 좋다.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
import express from 'express';
import fs from 'fs';
import fsAsync from 'fs/promises';
const app = express();
app.use(express.json());
app.get('/file1', (req, res) => {
// 1. 비동기함수 에러 처리 방법
fs.readFile('/file1.txt', (err, data) => {
if (err) {
res.status(404).send('File Not Found'); // 비동기함수는 첫번째 인자로 들어온 err 에 대한 처리를 해줘야한다. 처리 하지 않으면 클라이언트나 서버에 에러에 대한 로그를 잡을 수 없다.
}
});
// // 2. 동기함수 에러 처리 방법
// try {
// const data = fs.readFileSync('/file1.txt'); // 동기함수는 try{} catch(){} 로 감싸서 자세한 에러 내용을 클라이언트에게 알려주거나 혹은, 감싸지 않아도 마지막 에러 핸들러에서 에러 처리 가능
// } catch (error) {
// res.status(404).send('File not found');
// }
});
// 버전 5 이하에서는: require('express-async-errors');
// Express 5 부터는 이렇게
app.use((error, req, res, next) => {
console.error(error);
res.status(500).json({ message: 'Something went wrong' });
});
app.listen(8080);
import express from 'express'; import fs from 'fs'; import fsAsync from 'fs/promises'; const app = express(); app.use(express.json()); app.get('/file1', (req, res) => { // 1. 비동기함수 에러 처리 방법 fs.readFile('/file1.txt', (err, data) => { if (err) { res.status(404).send('File Not Found'); // 비동기함수는 첫번째 인자로 들어온 err 에 대한 처리를 해줘야한다. 처리 하지 않으면 클라이언트나 서버에 에러에 대한 로그를 잡을 수 없다. } }); // // 2. 동기함수 에러 처리 방법 // try { // const data = fs.readFileSync('/file1.txt'); // 동기함수는 try{} catch(){} 로 감싸서 자세한 에러 내용을 클라이언트에게 알려주거나 혹은, 감싸지 않아도 마지막 에러 핸들러에서 에러 처리 가능 // } catch (error) { // res.status(404).send('File not found'); // } }); // 버전 5 이하에서는: require('express-async-errors'); // Express 5 부터는 이렇게 app.use((error, req, res, next) => { console.error(error); res.status(500).json({ message: 'Something went wrong' }); }); app.listen(8080);
import express from 'express';
import fs from 'fs';
import fsAsync from 'fs/promises';

const app = express();

app.use(express.json());

app.get('/file1', (req, res) => {
  // 1. 비동기함수 에러 처리 방법
  fs.readFile('/file1.txt', (err, data) => {
    if (err) { 
      res.status(404).send('File Not Found'); // 비동기함수는 첫번째 인자로 들어온 err 에 대한 처리를 해줘야한다. 처리 하지 않으면 클라이언트나 서버에 에러에 대한 로그를 잡을 수 없다.
    }
  });

  // // 2. 동기함수 에러 처리 방법
  // try {
  //   const data = fs.readFileSync('/file1.txt'); // 동기함수는 try{} catch(){} 로 감싸서 자세한 에러 내용을 클라이언트에게 알려주거나 혹은, 감싸지 않아도 마지막 에러 핸들러에서 에러 처리 가능
  // } catch (error) {
  //   res.status(404).send('File not found');
  // }
});

// 버전 5 이하에서는: require('express-async-errors');

// Express 5 부터는 이렇게
app.use((error, req, res, next) => {
  console.error(error);
  res.status(500).json({ message: 'Something went wrong' });
});

app.listen(8080);

readFileSync()는 파일을 다 읽어야지 다음줄로 넘어가는 동기 함수이다. 이 경우에는 try{}catch(){}로 감싸서 자세한 에러 사항을 잡아내거나, 혹은 try-catch로 감싸지 않아도 마지막 안전망(에러 핸들러)에 포착된다.

하지만 readFile(‘/file1.txt’,(err,data)=>{})은 파일을 다 읽고 다음 줄로 넘어가는게 아니라, 실행시켜두고 파일이 다 읽어지면 등록한 콜백함수를 실행시켜주는 비동기 함수이다. ‘/file1.txt’ 가 다 읽어지면, 등록한 콜백함수의 첫번째 인자는 에러 발생시 인자로, 데이터를 두번째 인자로 넘겨 콜백함수를 실행해준다.

미들웨어 내의 비동기함수에서 err에 대한 핸들링을 해주지않으면 최후의 보루인 에러 핸들러에도 에러가 잡히지 않는다. 왜냐하면 readFile()을 호출하는 것 자체만으로는 에러가 발생하지않았고, 콜백함수 첫번째 인자(err)에 에러가 전달되었기 때문이다.

그래서 비동기 함수의 경우에는 콜백함수 내에서 에러에 대한 적절한 처리를 해주어야한다!


Promise 함수 에러 핸들링

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
app.get('/file2', (req, res) => {
fsAsync
.readFile('/file2.txt') //
.catch((error) => {
res.status(404).send('file2 not found!');
});
//.catch(next); // 최종 에러 핸들러가 있다면, error를 next로 전달해도 됨
});
app.use((error, req, res, next) => {
console.error(error);
res.status(500).json({ message: 'Something went wrong' });
});
app.get('/file2', (req, res) => { fsAsync .readFile('/file2.txt') // .catch((error) => { res.status(404).send('file2 not found!'); }); //.catch(next); // 최종 에러 핸들러가 있다면, error를 next로 전달해도 됨 }); app.use((error, req, res, next) => { console.error(error); res.status(500).json({ message: 'Something went wrong' }); });
app.get('/file2', (req, res) => {
  fsAsync
    .readFile('/file2.txt') //
    .catch((error) => {
      res.status(404).send('file2 not found!');
    });
    //.catch(next); // 최종 에러 핸들러가 있다면, error를 next로 전달해도 됨
});

app.use((error, req, res, next) => {
  console.error(error);
  res.status(500).json({ message: 'Something went wrong' });
});

Promise는 콜백함수를 등록하지는 않지만, .then((data)=>{}).catch((error)=>{}) 와 같이 정상적으로 잘 동작했다면 data를 받아서 then을 실행하고 에러가 나면catch 의 인자로 error 를 받아 에러를 처리한다.

Promise 코드를 호출한다고 해서, 그 호출하는 코드에 문제가 있는 것은 아니기 때문에 에러가 발생하지 않고, 추후 비동기로 함수가 실행되는중 에러가 발생하면 catch의 인자로써 들어가기때문에 에러가 잡히지 않는다. 비동기함수는 try-catch 로 에러를 잡을 수 없다. (함수 실행 자체에는 문제가 없고 추후, 내부에서 에러가 발생하기 때문)

그래서 Promise 함수의 경우에는 .then().catch()의 catch 내에서 에러 핸들링을 해줘야한다. 에러를 next(error); 로 전달해도 되고, res를 직접 보내도 된다.

async / await 에러 핸들링

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
app.get('/file3', async (req, res) => {
try {
const data = await fsAsync.readFile('/file2.txt');
} catch {
res.sendStatus(404);
}
});
app.use((error, req, res, next) => {
console.error(error);
res.status(500).json({ message: 'Something went wrong' });
});
app.listen(8080);
app.get('/file3', async (req, res) => { try { const data = await fsAsync.readFile('/file2.txt'); } catch { res.sendStatus(404); } }); app.use((error, req, res, next) => { console.error(error); res.status(500).json({ message: 'Something went wrong' }); }); app.listen(8080);
app.get('/file3', async (req, res) => {
  try {
    const data = await fsAsync.readFile('/file2.txt');
  } catch {
    res.sendStatus(404);
  }
});

app.use((error, req, res, next) => {
  console.error(error);
  res.status(500).json({ message: 'Something went wrong' });
});

app.listen(8080);
const data = await fsAsync.readFile('/file2.txt');

위 코드는 file2.txt 를 읽어오는걸 기다렸다가 data 변수에 담는 것으로 동기적으로 실행된다.

async로 함수를 감싸주면, 함수 내부에서는 await 라는 키워드를 이용하여, 순차적(동기적)으로 실행되는 것처럼 처리할 수 있지만, 함수 자체는 비동기인 Promise로 감싸진다. 그렇기 때문에 async 콜백함수 내부에서 에러가 발생하면 Promise 내에서 에러가 발생하는것과 동일하기때문에 catch를 이용해서 잡아야한다.


비동기 에러 처리하는 방법 – 최신 버전

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
import express from 'express';
import fsAsync from 'fs/promises';
const app = express();
app.get('/', (req, res, next) => {
return fsAsync.readFile('/file2.txt').catch(next);
});
app.use((error, req, res, next) => {
console.error(error);
res.status(500).json({ message: 'Something went wrong' });
next();
});
//github.com/expressjs/express/issues/2259#issuecomment-433586394
//github.com/blakeembrey/async-middleware
app.listen(8080);
import express from 'express'; import fsAsync from 'fs/promises'; const app = express(); app.get('/', (req, res, next) => { return fsAsync.readFile('/file2.txt').catch(next); }); app.use((error, req, res, next) => { console.error(error); res.status(500).json({ message: 'Something went wrong' }); next(); }); //github.com/expressjs/express/issues/2259#issuecomment-433586394 //github.com/blakeembrey/async-middleware app.listen(8080);
import express from 'express';
import fsAsync from 'fs/promises';

const app = express();

app.get('/', (req, res, next) => {
  return fsAsync.readFile('/file2.txt').catch(next);
});

app.use((error, req, res, next) => {
  console.error(error);
  res.status(500).json({ message: 'Something went wrong' });
  next();
});

//github.com/expressjs/express/issues/2259#issuecomment-433586394
//github.com/blakeembrey/async-middleware

app.listen(8080);

비동기 함수에서 catch를 사용하지 않더라도, 마지막 안전망(에러 처리 미들웨어)에 에러를 떨어트릴 수 있는 방법이 있다.

express 버전 5 미만은, ‘express-async-error’ 모듈을 사용하여, promiss를 리턴하기만 하면 안전망에서 에러를 잡을 수 있다.

express 버전 5부터는 promiss를 리턴해도 별 다른 에러처리를 하지 않는다면, 제일 마지막 안전망에서 에러를 처리할 수 있다.


중요한 포인트 ✏️

  • 각각의 미들웨어에서 에러가 발생했을때 적절한 에러메세지를 사용자에게 보내줘야 한다.
  • 동기 / 비동기 에러 처리하는 방법이 다르다
    • 동기 함수의 경우 실수로 에러처리를 하지않더라도 마지막 안전망(에러 처리 미들웨어)가 에러를 포착할 수 있지만, Promise 혹은 Async(비동기적)을 쓸때 에러가 발생한다면, 외부에서는 에러를 감지할 수 있는 방법이 없다.