[Node.js] Express Error Handling

유동균·2023년 2월 3일
0

Node.js

목록 보기
3/11
post-thumbnail

1. Error Handling이란?

  • Express의 Error Handling은 응용 프로그램에서 발생할 수 있는 오류를 캡쳐하고 처리하는 과정을 말한다.
  • 웹 애플리케이션을 개발할 때, 어떤 오류가 발생할 수 있는데
    응용 프로그램의 코드에서 즉시 발생하는 동기적 오류와, 네트워크 시간 초과나 외부 API 오류 등 응용 프로그램의 정상 흐름에서 벗어나는 비동기적 오류들이 있다.
  • Error Handling은 이러한 오류가 발생할 때, 올바른 응답을 보내는 것을 말한다.
    예를 들어, Error 메시지를 표시하거나, 사용자를 적절한 페이지로 리다이렉트하는 것.
  • Express는 기본적인 Error Handling를 제공해서 오류를 처리할 수 있지만,
    개발자가 오류에 대한 사용자 정의 응답을 제공하거나 디버깅을 위해 오류를 기록하는 것을 사용자 정의할 수도 있다.

2. Error Handling이 필요한 이유

  • Error Handling이 필요한 이유는 오류가 발생하면 웹 애플리케이션이 제대로 작동하지 않고, 사용자에게 알 수 없는 오류 메시지가 나타날 수 있기 때문이다.
  • 따라서, 오류가 발생할 경우 사용자에게 적절한 응답을 하고, 오류 원인을 분석하는 것이 중요하다.
  • Error Handling 캡처 및 처리 메커니즘이 있는 것은 오류가 발생할 경우라도 애플리케이션이 안정적이고 일관적인 사용자 경험을 제공하는 것이 중요하기 때문이다.

3. Catching Errors

  • route handler와 middleware가 실행되는 동안 발생하는 모든 오류를 Express가 잡아서 처리할 수 있도록 확인하는 것이 중요하다.

    route handler
    Express에서 route handlers는 어플리케이션의 특정 경로(또는 엔드포인트)에 대해 수신되는 HTTP 요청을 처리하는 함수이다.
    이는 어플리케이션의 라우팅 구성에 정의되어 있으며, 클라이언트에게 응답을 보내거나 요청을 다음 middleware 함수에 전달하는 책임이 있다.
    Route handlers는 수신되는 요청과 응답 객체를 매개변수로 받아, 이 객체를 조작하여 클라이언트에 응답을 보낼 수 있고,
    개발자가 어플리케이션의 다른 경로에 대한 특정 동작을 정의할 수 있게 해주어, 동적이고 유연한 API와 웹 애플리케이션을 구축할 수 있게 한다.

    const express = require('express');
    const app = express();
    app.get('/', (req, res) => {
      res.send('Hello World!');
    });
    app.listen(3000, () => {
      console.log('Example app listening on port 3000!');
    });
    //  app.get('/', (req, res) => { ... }) 는 '/' 경로로의 수신되는 GET 요청을 듣는 route handler. 
    // 요청이 수신되면 핸들러 함수 (req, res) => { ... } 가 실행 
    // 이 함수는 "Hello World!" 라는 메시지와 함께 클라이언트에 응답을 보냄.
  • route handlers와 middleware 내부의 동기적 코드에서 발생하는 오류는 추가적인 작업 없이도 처리되고, 동기적 코드가 Error를 발생시키면, Express가 이를 잡아서 처리한다.

app.get('/', (req, res) => {
  throw new Error('BROKEN') // Express가 Error를 잡아서 처리한다.
})

  • 아래의 코드는 route handlers와 middleware에서 호출한 비동기 함수에서 발생한 에러를 처리하는 방법.
    이러한 에러는 next() 함수에 전달되어야하며, Express가 에러를 잡고 처리할 수 있다.
app.get('/', (req, res, next) => {
  fs.readFile('/file-does-not-exist', (err, data) => {
    if (err) {
      next(err) // Express로 Error전달
    } else {
      res.send(data)
    }
  })
})
  • Express 5 이상에서, route handlers와 middleware가 Promise를 반환하는 경우, 에러를 반환하거나 발생시키면 자동으로 next(value)를 호출한다.
app.get('/user/:id', async (req, res, next) => {
  const user = await getUserById(req.params.id)
  res.send(user)
})
  • next() 함수에 getUserById가 Error를 던지거나 거부하면, 던진 Error나 거부된 값으로 next가 호출.
    거부된 값이 제공되지 않으면, Express 라우터가 제공하는 기본 Error 객체로 next가 호출.

  • next() 함수에 'route' 문자열을 제외한 다른 것을 전달하면, Express는 현재 요청이 Error로 간주되고, 나머지 Error 처리 라우팅 및 middleware 기능은 건너뛴다.

  • 콜백이 sequence에서 데이터를 제공하지 않고 오직 Error만 제공하는 경우, 코드를 다음과 같이 간소화할 수 있습니다.

    sequence
    여러 개의 middleware 함수 또는 route handler가 순서대로 실행되는 것을 말함
    요청이 들어오면 각 middleware 함수 또는 route handler가 순차적으로 실행되어 응답을 제공
    각 단계에서 다음 middleware 함수 또는 route handler로 요청이 전달되면서 처리가 완료

app.get('/', [
  function (req, res, next) {
    fs.writeFile('/inaccessible-path', 'data', next)
  },
  function (req, res) {
    res.send('OK')
  }
])
  • 위 예제에서는 fs.writeFile의 콜백에 next가 제공됨.
  • 이 함수는 오류 여부에 관계없이 호출. 오류가 없으면 두 번째 핸들러가 실행되고, 오류가 있으면 Express가 오류를 잡아서 처리합니다.

  • route handler와 middleware에서 호출된 비동기 코드에서 발생하는 에러는 캐치하여 Express에 처리할 수 있도록 해야한다.
app.get('/', (req, res, next) => {
  setTimeout(() => {
    try {
      throw new Error('BROKEN')
    } catch (err) {
      next(err)
    }
  }, 100)
})
  • 위의 예제에서는 try...catch 블록을 사용하여 라우트 핸들러 또는 미들웨어에서 호출하는 비동기 코드의 오류를 잡아 Express에 전달한다.
  • try...catch 블록이 생략되었다면 Express는 동기 핸들러 코드의 일부가 아니기 때문에 오류를 잡지 않는다.

  • Promise를 사용하면 try...catch 블록의 오버헤드를 피하거나 Promise를 반환하는 함수를 사용할 때 편리
app.get('/', (req, res, next) => {
  Promise.resolve().then(() => {
    throw new Error('BROKEN')
  }).catch(next) // Error는 Express에 전달
})
  • Promise는 동기적 에러와 거부된 Promise 모두를 자동으로 잡으므로 next를 마지막 catch 핸들러로 제공하기만 하면, 첫 번째 인수로 에러가 제공되는 catch 핸들러인 Express가 에러를 잡을 수 있다.

  • 동기적 오류 처리를 통해 핸들러 체인을 사용할 수도 있으며, 비동기 코드를 단순화하여 동기적 오류 처리에 의존할 수 있다.
app.get('/', [
  function (req, res, next) {
    fs.readFile('/maybe-valid-file', 'utf-8', (err, data) => {
      res.locals.data = data
      next(err)
    })
  },
  function (req, res) {
    res.locals.data = res.locals.data.split(',')[1]
    res.send(res.locals.data)
  }
])
  • 위의 예제는 readFile 호출로부터 두 개의 간단한 문장.
  • readFile이 error를 일으키면, Express에게 error를 전달하고, 그렇지 않으면 다음 핸들러로 빠르게 돌아가서 동기 에러 프로세싱 영역으로 돌아간다.
  • 그런 다음 위 예제에서는 데이터를 처리하고, 실패하면 동기 error handler가 error를 잡는다.
  • 만약 readFile의 콜백 내부에서 이 프로세싱을 했다면, 애플리케이션이 종료될 수 있으며 Express 에러 핸들러들이 실행되지 않을 수 있다.

  • 어떤 방법을 사용하든, Express 에러 핸들러가 호출되고 애플리케이션이 생존하게 하려면, Express가 에러를 수신하도록 확실히 해야한다.

4. The default error handler

  • Express에는 애플리케이션에서 발생할 수 있는 오류를 처리하는 내장 오류 핸들러가 있다.
    이 기본 오류 핸들링 미들웨어 함수는 미들웨어 함수 스택의 끝에 추가해야한다.
  • next()를 호출하여 오류를 전달하면, 사용자 정의 오류 핸들러에서 처리하지 않은 경우 내장 오류 핸들러가 처리한다.

  • 에러가 발생하면, 다음 정보가 응답에 추가:
    • res.statusCodeerr.status(또는 err.statusCode)로부터 설정. 이 값이 4xx 또는 5xx 범위 외의 값일 경우, 500으로 설정됨.
    • res.statusMessage는 상태 코드에 따라 설정됨.
    • body는 프로덕션 환경일 때 상태 코드 메시지의 HTML, 그렇지 않으면 err.stack.
    • err.headers 객체에서 지정한 헤더.

  • 만약 응답을 스트리밍하는 도중에 에러가 발생한다면(예를 들어 클라이언트로의 응답 스트리밍 중에 에러가 발생할 경우),
    next() 함수를 호출하여 Express 기본 에러 핸들러가 연결을 끊고 요청을 실패시킨다.

  • 그래서 사용자 정의 에러 핸들러를 추가할 때, 헤더가 클라이언트에 이미 전송되었을 때 기본 Express 에러 핸들러로 위임해야함.

function errorHandler (err, req, res, next) {
  if (res.headersSent) {
    return next(err)
  }
  res.status(500)
  res.render('error', { error: err })
}
  • 주의해야 할 것은, 코드에서 에러가 발생하여 next()를 호출하는 경우, 사용자 정의 에러 핸들링 미들웨어가 있더라도 기본 에러 핸들러가 트리거될 수 있다.

5. Writing error handlers

  • 에러 핸들링 미들웨어 함수는 다른 미들웨어 함수처럼 정의하되, 에러 핸들링 함수는 세 개의 인자 대신 네 개의 인자가 있다. (err, req, res, next).
app.use((err, req, res, next) => {
  console.error(err.stack)
  res.status(500).send('Something broke!')
})
  • 에러 핸들링 미들웨어는 다른 app.use() 및 경로 호출 후에 마지막으로 정의해야한다.
const bodyParser = require('body-parser')
const methodOverride = require('method-override')

app.use(bodyParser.urlencoded({
  extended: true
}))
app.use(bodyParser.json())
app.use(methodOverride())
app.use((err, req, res, next) => {
  // logic
})
  • Middleware 함수에서의 응답은 HTML 오류 페이지, 간단한 메시지 또는 JSON 문자열과 같은 어떤 형식이든 사용할 수 있다.

  • 조직적인 (그리고 높은 수준의 프레임워크) 목적으로, 일반 Middleware 함수처럼 다수의 오류 처리 Middleware 함수를 정의할 수 있는데,
    예를 들어, XHR을 사용하여 요청한 것과 XHR을 사용하지 않은 요청을 위한 오류 핸들러를 정의하는 것이다.
const bodyParser = require('body-parser')
const methodOverride = require('method-override')

app.use(bodyParser.urlencoded({
  extended: true
}))
app.use(bodyParser.json())
app.use(methodOverride())
app.use(logErrors)
app.use(clientErrorHandler)
app.use(errorHandler)
  • generic logErrors는 예를 들어 stderr에 요청 및 오류 정보를 기록할 수 있다.
function logErrors (err, req, res, next) {
  console.error(err.stack)
  next(err)
}
  • 이 예제에서 clientErrorHandler는 다음과 같이 정의됩니다. 이 경우 에러는 명시적으로 다음으로 전달됩니다.
  • error-handling 함수에서 "next"를 호출하지 않으면, 응답을 작성하고 종료하는 책임이 있습니다. 그렇지 않으면, 요청이 정지 되어 가비지 컬렉션에서 예외처리되지 않습니다.
function clientErrorHandler (err, req, res, next) {
  if (req.xhr) {
    res.status(500).send({ error: 'Something failed!' })
  } else {
    next(err)
  }
}
  • 'catch-all' error Handler 함수를 구현
function errorHandler (err, req, res, next) {
  res.status(500)
  res.render('error', { error: err })
}
  • route 파라미터를 사용하여 다음 라우트 핸들러로 건너 뛸 수 있습니다
app.get('/a_route_behind_paywall',
  (req, res, next) => {
    if (!req.user.hasPaid) {
      // continue handling this request
      next('route')
    } else {
      next()
    }
  }, (req, res, next) => {
    PaidContent.find((err, doc) => {
      if (err) return next(err)
      res.json(doc)
    })
  })
  • 이 예제에서, getPaidContent 핸들러는 건너뛰어지지만 /a_route_behind_paywall에 대한 app에서 남은 핸들러는 계속 실행될 것입니다.

https://blog.siner.io/2020/01/04/express-middleware/
https://velog.io/@younoah/nodejs-express-error

Express에서 Error Handling은 애플리케이션에서 발생하는 오류를 처리하는 것을 말합니다. 이는 적절한 오류 메시지를 클라이언트에 전송하거나, 애플리케이션의 로깅 정보에 기록하는 것 등을 포함할 수 있습니다. Express에서는 미들웨어 함수를 사용하여 오류 핸들링을 수행할 수 있습니다.
Express에서 Error Handling은 애플리케이션의 에러처리를 위한 방법입니다. 에러 핸들링을 위한 미들웨어를 정의하고, 에러가 발생하면 기본적으로 정의된 에러 핸들링 미들웨어를 호출하는 것이 일반적입니다. 만약 커스텀 에러 핸들링 미들웨어를 정의하면 기본 에러 핸들링 미들웨어 대신 커스텀 에러 핸들링 미들웨어를 호출할 수 있습니다.

Express에서 Error Handling은 어플리케이션에서 발생하는 에러를 처리하기 위한 기능입니다. Error Handling을 정의하는 방법은 아래와 같습니다.
1. 에러 핸들링 미들웨어 정의: Error Handling을 위한 미들웨어를 정의할 수 있습니다. Error Handling 미들웨어는 일반 미들웨어와 다르게 4개의 인자(err, req, res, next)를 갖습니다.
2. 에러 핸들링 미들웨어 호출: app.use() 호출 뒤에 에러 핸들링 미들웨어를 정의하며, next() 함수를 통해 에러 핸들링 미들웨어를 호출할 수 있습니다.
3. 에러 메시지 작성: Error Handling 미들웨어에서 res.write() 혹은 res.send() 등의 메시지를 작성하여, 에러 메시지를 전달할 수 있습니다.
4. 응답 종료: Error Handling 미들웨어에서 next() 함수를 호출하지 않은 경우, res.end() 를 호출하여 응답을 종료해야 합니다. 그렇지 않으면, 요청이 "막히" 되어 가비지 컬렉션

Express에서 커스텀 에러 핸들링 미들웨어는 개발자가 정의하고 구현한 에러 처리 로직을 가진 미들웨어입니다. 이는 애플리케이션에서 발생하는 에러를 처리하는데 사용되며, 애플리케이션의 기본 에러 핸들링 기능을 확장하거나 대체할 수 있습니다.
Express에서 커스텀 에러 핸들링 미들웨어를 정의하는 방법은 다음과 같습니다:
1. 에러 객체를 전달할 수 있는 함수를 작성합니다. 이 함수는 에러 객체를 처리하고 원하는 형식으로 응답을 보냅니다.
2. 이 함수를 app.use() 메서드에 추가합니다.
3. 응용 프로그램 로직에서 에러가 발생하면, next(err) 메서드를 호출하여 에러 객체를 전달합니다.

Express에서 커스텀 에러 핸들링 미들웨어의 예시

  1. Global error handler: 전역 에러 핸들링 미들웨어는 어플리케이션 내의 모든 에러를 처리할 수 있습니다. 아래와 같은 코드로 정의할 수 있습니다.
app.use(function(err, req, res, next) {
  console.error(err.stack);
  res.status(500).send('Something broke!');
});
  1. Route-level error handler: 라우트 레벨 에러 핸들링 미들웨어는 특정 라우트에서 발생한 에러를 처리할 수 있습니다. 아래와 같은 코드로 정의할 수 있습니다.
app.get('/a_route', function(req, res, next) {
  try {
    // some code
  } catch (err) {
    next(err);
  }
});

app.use('/a_route', function(err, req, res, next) {
  console.error(err.stack);
  res.status(500).send('Something broke!');
});
  1. Asynchronous error handler: 비동기 에러 핸들링 미들웨어는 비동기 함수에서 발생한 에러를 처리할 수 있습니다. 아래와 같은 코드로 정의할 수 있습니다.
app.get('/async_route', async function(req, res, next) {
  try {
    // some asynchronous code
  } catch (err) {
    next(err);
  }
});

app.use(function(err, req, res, next) {
  console.error(err.stack);
  res.status(500).send('Something broke!');
});
  1. try-catch 문을 사용한 에러 핸들링:
app.use((req, res, next) => {
  try {
    // Your code here
    next();
  } catch (error) {
    next(error);
  }
});
  1. 에러 처리 미들웨어를 사용한 에러 핸들링:
app.use((error, req, res, next) => {
  // Your error handling logic here
  res.status(500).json({ message: error.message });
});
  1. 에러 클래스를 사용한 에러 핸들링:
class CustomError extends Error {
  constructor(message) {
    super(message);
    this.statusCode = 400;
  }
}

app.use((error, req, res, next) => {
  const statusCode = error.statusCode || 500;
  res.status(statusCode).json({ message: error.message });
});

// In your route handlers
if (error) {
  throw new CustomError('Invalid request data');
}
  1. 정적 파일 요청 에러 핸들링:
app.use(express.static(path.join(__dirname, 'public')));

app.use((req, res, next) => {
  const error = new Error('File not found');
  error.status = 404;
  next(error);
});

app.use((error, req, res, next) => {
  res.status(error.status || 500);
  res.send(error.message);
});
  1. 유저 입력 에러 핸들링:
app.use((req, res, next) => {
  if (!req.body.username) {
    const error = new Error('Username is required');
    error.status = 400;
    next(error);
  }
});

app.use((error, req, res, next) => {
  res.status(error.status || 500);
  res.send(error.message);
});
  1. 특정 에러 타입에 대한 커스텀 에러 핸들링
const express = require('express');
const app = express();

app.use((req, res, next) => {
  const error = new Error('Not Found');
  error.status = 404;
  next(error);
});

app.use((error, req, res, next) => {
  if (error.status === 404) {
    res.status(404).send('Not Found');
  } else {
    next(error);
  }
});
  1. 에러 메시지를 개발 모드에서만 보여주는 커스텀 에러 핸들링
const express = require('express');
const app = express();

app.use((req, res, next) => {
  const error = new Error('Not Found');
  error.status = 404;
  next(error);
});

app.use((error, req, res, next) => {
  res.status(error.status || 500);
  if (process.env.NODE_ENV === 'development') {
    res.send({
      error: {
        message: error.message,
        status: error.status,
        stack: error.stack
      }
    });
  } else {
    res.send({
      error: {
        message: error.message,
        status: error.status
      }
    });
  }
});
  1. 상세한 로깅을 포함한 커스텀 에러 핸들링
const express = require('express');
const app = express();
const logger = require('morgan');

app.use(logger('dev'));

app.use((req, res, next) => {
  const error = new Error('Not Found');
  error.status = 404;
  next(error);
});

app.use((error, req, res, next) => {
  res.status(error.status || 500);
  res.send({
    error: {
      message: error.message,
      status: error.status
    }
  });
});

  • Express에서 에러 핸들링은 다음과 같은 구동 방식으로 이루어집니다:
  1. 요청-응답 처리 과정에서 예외가 발생합니다.
  2. Express에서 제공하는 next 함수를 통해 예외 상황을 에러 핸들링 미들웨어로 전달합니다.
  3. 정의된 에러 핸들링 미들웨어가 예외 상황을 처리하여 적절한 응답을 제공합니다.
    에러 핸들링 미들웨어는 요청-응답 처리 과정에서 발생한 예외를 처리하기 위해 사용됩니다. 적절한 응답을 제공하지 않을 경우, 사용자에게 500 Internal Server Error와 같은 내부 서버 오류 메시지가 제공될 수 있습니다.
  • next함수란
    Express에서 next 함수는 미들웨어 체인에서 다음 미들웨어로 제어권을 전달하기 위해 사용됩니다. 에러 핸들링에서는 에러가 발생했음을 나타내고, 에러 핸들링을 체인에서의 다음 에러 핸들링 미들웨어에 위임하기 위해 많이 사용됩니다.
    next 함수는 각 미들웨어 함수에 인수로 전달되며, 에러를 인수로 하여 호출할 수 있습니다. next가 에러를 인수로 가지면서 호출되면, 이후의 에러 핸들링 미들웨어에서 에러를 수신하고 적절하게 처리할 수 있습니다.
    다음은 Express의 에러 핸들링 미들웨어에서 next 함수를 사용하는 예시입니다:
app.use(function (err, req, res, next) {
  console.error(err.message);
  res.status(500).send('Something went wrong');
});

이 예시에서, 이전의 미들웨어에서 next에 에러가 전달되면, 이 에러 핸들링 미들웨어에서 이 에러를 받아서 콘솔에 에러 메시지를 기록합니다. 그런 다음 500 상태 코드를 가진 응답이 보내지며, 내부 서버 오류가 발생했음을 의미합니다.

  • 미들웨어 체인이란
    Express에서 미들웨어 체인은 특정 경로에 대해 실행되는 미들웨어 함수의 순서를 말합니다. 미들웨어 체인의 각 미들웨어 함수는 정의된 순서대로 호출되며, 요청과 응답 객체는 하나의 미들웨어에서 다음 미들웨어로 전달됩니다.
    미들웨어 체인은 요청이 통과하는 파이프라인으로 생각할 수 있습니다. 각 미들웨어 체인에 있는 미들웨어는 요청 데이터 해석, 사용자 인증 또는 에러 처리 등의 작업을 수행할 기회를 갖습니다.
    여러 미들웨어 함수를 체인으로 연결함으로써, 각 요청에 대해 수행되는 복잡한 작업 시리즈를 구축하고, Express 애플리케이션에서 기능을 모듈화하고 재사용하기 쉽습니다.
const express = require('express');
const app = express();

// Middleware 1: Parses request data
app.use(express.json());

// Middleware 2: Adds a header to the response
app.use((req, res, next) => {
  res.header('X-Middleware', '2');
  next();
});

// Middleware 3: Logs the request method
app.use((req, res, next) => {
  console.log(`Request method: ${req.method}`);
  next();
});

// Route handler
app.get('/', (req, res) => {
  res.send('Hello, World!');
});

app.listen(3000, () => {
  console.log('Example app listening on port 3000!');
});
  • 미들웨어 체인의 예시
    • 이 예제에서는 정의된 순서대로 세 개의 미들웨어 함수가 실행됩니다.
    • 첫 번째 미들웨어는 express.json() 메소드를 사용하여 요청 데이터를 파싱합니다.
    • 두 번째 미들웨어는 응답에 X-Middleware 헤더를 추가합니다.
    • 세 번째 미들웨어는 요청 메소드를 콘솔에 기록합니다.
    • 마지막으로, '/' 엔드포인트에 대한 경로 핸들러가 "Hello, World!" 응답을 보냅니다.
    • '/' 엔드포인트에 요청이 전송되면, 미들웨어 체인의 각 미들웨어는 순서대로 실행되고, 요청 및 응답 객체는 한 미들웨어에서 다음 미들웨어로 전달됩니다.
    • 이를 통해 각 미들웨어는 요청 또는 응답에 대한 작업을 수행한 후, 미들웨어 체인의 다음 미들웨어에 제어를 전달할 수 있습니다.
  • 미들웨어란
    Express에서, 미들웨어는 HTTP 요청과 응답 사이에 위치하여 요청 또는 응답을 처리하는 기능을 구현하는 함수입니다. 미들웨어 함수는 Express 애플리케이션에서 구성된 경로(route)에 대해 실행될 수 있으며, 각 미들웨어는 순차적으로 정의된 순서대로 호출됩니다.
    미들웨어는 요청 또는 응답 객체에 작업을 수행할 수 있으며, 예를 들어 요청 데이터 파싱, 사용자 인증, 에러 처리 등의 기능을 구현할 수 있습니다. 여러 개의 미들웨어를 체인으로 연결하여, 각 요청에 대해 수행할 복잡한 작업 시나리오를 구성하고, Express 애플리케이션에서 기능을 재사용하는 것이 가능합니다.

0개의 댓글