[Koa] Error Handling
정의
Koa Error Handling 은 미들웨어 chain 에서 발생한 예외를 catch 하여 응답을 정형화하는 패턴입니다. Koa 는 자동 error handler 를 제공하지만, 프로덕션은 대개 커스텀 error middleware + app.on('error') 로직을 조합합니다.
기본: try/catch middleware
가장 관용적 패턴. 모든 미들웨어의 최외곽.
app.use(async (ctx, next) => {
try {
await next();
} catch (err) {
ctx.status = err.status || err.statusCode || 500;
ctx.body = {
error: err.message,
...(process.env.NODE_ENV !== 'production' && {stack: err.stack}),
};
ctx.app.emit('error', err, ctx);
}
});
app.on('error') 는 로깅/외부 알림 (Sentry) 용:
app.on('error', (err, ctx) => {
logger.error({err, url: ctx?.url});
Sentry.captureException(err);
});
ctx.throw()
http-errors 기반 예외 발생. Status + message + properties:
ctx.throw(400); // 400 Bad Request
ctx.throw(400, 'Missing email');
ctx.throw(404, 'User not found');
ctx.throw(422, 'Validation failed', {
field: 'email',
code: 'INVALID_EMAIL',
});
Error 객체 자체:
const err = new Error('boom');
err.status = 400;
throw err;
ctx.assert()
Truthy 검사 + 실패 시 throw:
ctx.assert(user, 401, 'Login required');
ctx.assert(id, 400, 'ID required');
ctx.assert(admin, 403);
assert(user, 401) == if (!user) ctx.throw(401).
Async error
Async middleware 에서 throw 는 자동으로 upstream 으로. await 없이 던지면 위험:
// BAD
app.use(async (ctx, next) => {
await next();
});
app.use(async (ctx) => {
someAsyncFn().catch(err => { throw err; }); // upstream 안 감
});
await 을 반드시:
app.use(async (ctx) => {
await someAsyncFn(); // 실패 시 자동 upstream
});
응답 형식 통일
app.use(async (ctx, next) => {
try {
await next();
} catch (err) {
const status = err.status || err.statusCode || 500;
const isProduction = process.env.NODE_ENV === 'production';
ctx.status = status;
ctx.body = {
error: {
code: err.code || (status === 500 ? 'INTERNAL' : 'ERROR'),
message: (isProduction && status === 500) ? 'Internal Server Error' : err.message,
...(err.details && {details: err.details}),
...(!isProduction && {stack: err.stack}),
},
timestamp: new Date().toISOString(),
path: ctx.path,
requestId: ctx.state.requestId,
};
if (status >= 500) {
ctx.app.emit('error', err, ctx);
}
}
});
Custom Error class
class DomainError extends Error {
constructor(code, message, status = 400, details) {
super(message);
this.code = code;
this.status = status;
this.details = details;
this.name = 'DomainError';
}
}
class UserNotFoundError extends DomainError {
constructor(id) {
super('USER_NOT_FOUND', `User ${id} not found`, 404);
this.userId = id;
}
}
// 사용
router.get('/users/:id', async (ctx) => {
const user = await User.findById(ctx.params.id);
if (!user) throw new UserNotFoundError(ctx.params.id);
ctx.body = user;
});
Error middleware 에서 특정 error 매핑:
if (err instanceof PrismaClientKnownRequestError) {
if (err.code === 'P2002') {
ctx.status = 409;
ctx.body = {error: 'Duplicate entry'};
return;
}
}
Validation error (Zod)
const {ZodError} = require('zod');
app.use(async (ctx, next) => {
try {
await next();
} catch (err) {
if (err instanceof ZodError) {
ctx.status = 400;
ctx.body = {
error: 'Validation failed',
details: err.errors,
};
return;
}
throw err; // 다음 handler 로
}
});
404 처리
Router 가 매칭 실패 시 default 로 404. 커스텀:
app.use(async (ctx, next) => {
await next();
if (ctx.status === 404 && !ctx.body) {
ctx.body = {error: 'Not found', path: ctx.path};
}
});
또는 router 뒤에 catch-all:
app.use(router.routes());
app.use(router.allowedMethods());
app.use(async (ctx) => {
ctx.status = 404;
ctx.body = {error: 'Not found'};
});
app.on('error')
Middleware chain 안 오류 + Koa 자체 오류 모두 emit.
app.on('error', (err, ctx) => {
// ctx 는 있을 수도 없을 수도 (chain 밖 에러는 없음)
const info = {
err: {
message: err.message,
code: err.code,
status: err.status,
stack: err.stack,
},
request: ctx ? {
method: ctx.method,
url: ctx.url,
headers: ctx.headers,
ip: ctx.ip,
} : null,
};
logger.error(info);
});
Unhandled Rejection / Uncaught Exception
Middleware 밖 에러 (setTimeout, background task):
process.on('unhandledRejection', (reason, promise) => {
logger.error('Unhandled Rejection:', reason);
Sentry.captureException(reason);
});
process.on('uncaughtException', (err) => {
logger.error('Uncaught Exception:', err);
Sentry.captureException(err);
// 프로세스 재시작 권장 (일관 상태 보장 불가)
process.exit(1);
});
Client 오류 vs Server 오류
- 4xx (client): 로그 낮은 level. Alert X.
- 5xx (server): 로그 error level. Alert O.
if (status >= 500) {
logger.error({err});
Sentry.captureException(err);
} else {
logger.warn({err});
}
Timeout
const timeout = require('koa-timeout-v2');
app.use(timeout(30000, {status: 408}));
Long-running handler 는 별도 (queue 로 오프로드).
Error middleware 순서
최외곽에 (첫 미들웨어). 그래야 downstream 모든 에러 catch.
app.use(errorMiddleware); // 1
app.use(logger); // 2
app.use(bodyParser()); // 3
app.use(auth); // 4
app.use(router.routes()); // 5
Multiple error handlers
에러 종류별로:
// Layer 1: Domain errors
app.use(async (ctx, next) => {
try { await next(); } catch (err) {
if (err instanceof DomainError) {
ctx.status = err.status;
ctx.body = {code: err.code, message: err.message};
return;
}
throw err;
}
});
// Layer 2: Validation errors
app.use(async (ctx, next) => {
try { await next(); } catch (err) {
if (err instanceof ZodError) {
ctx.status = 400;
ctx.body = {errors: err.errors};
return;
}
throw err;
}
});
// Layer 3: Fallback (unknown)
app.use(async (ctx, next) => {
try { await next(); } catch (err) {
ctx.status = 500;
ctx.body = {error: 'Internal Server Error'};
ctx.app.emit('error', err, ctx);
}
});
함정
WARNING
Error middleware 를 마지막에 등록하면 못 잡음. 반드시 첫 번째.
CAUTION
Async 안 throw 는 promise rejection. await 없이 next 하면 lost.
WARNING
Prod 에서 stack trace 노출 위험. Env 로 분기.
IMPORTANT
ctx.throw() vs custom Error. throw 는 http-errors 기반. Custom error 는 별도 인스턴스. Middleware 에서 두 유형 모두 처리.
CAUTION
ctx.body 를 error middleware 앞 에서 세팅 후 throw = ctx.body 유지 X. Downstream 이 override.
관련 위키
- Koa.js - 상위 개요
- Koa Middleware - Error middleware 순서
- Koa Context - ctx.throw
- Koa Router
- Koa Body Parsing
- Koa Testing - error 테스트
- NestJS Exception Filters - 대비
- FastAPI Middleware - exception_handler 대비
이 글의 용어 (8개)
- [FastAPI] Middlewarefastapi
- 정의 FastAPI Middleware 는 모든 요청과 응답을 가로채 로깅, 인증, 압축, CORS 등 공통 처리를 하는 계층입니다. Starlette 의 ASGI middlew…
- [Framework] Koa.jsframeworks
- 정의 Koa.js 는 Express 창시자 TJ Holowaychuk 이 2013년 발표한 Node.js 웹 프레임워크 입니다. Express 의 후계자 성격이며, 미들웨어를 a…
- [Koa] Body Parsingframeworks
- 정의 Koa Body Parsing 은 HTTP 요청 body 를 파싱해 에 사용 가능한 형태로 만드는 미들웨어입니다. Koa 코어는 body parser 를 포함하지 않으므로 …
- [Koa] Context (ctx)frameworks
- 정의 Koa Context ( ) 는 각 요청마다 생성되어 미들웨어와 route handler 에 전달되는 객체입니다. (Node 원본), (Koa 확장), (Koa 확장), (…
- [Koa] Middleware (Onion Model)frameworks
- 정의 Koa Middleware 는 요청/응답 사이에 실행되는 async 함수입니다. 각 미들웨어는 (context) 와 (다음 미들웨어를 실행하는 함수) 를 받고, 로 down…
- [Koa] Router (@koa/router)frameworks
- 정의 @koa/router 는 Koa 의 공식 라우터입니다. Koa 코어는 라우팅을 제공하지 않으므로 별도 설치가 필요합니다. Express 스타일 라우팅 (HTTP method…
- [Koa] Testing (Supertest, Jest, Vitest)frameworks
- 정의 Koa Testing 은 (HTTP 호출) + / (assertions/runner) 조합이 표준입니다. 을 Supertest 에 전달하면 실제 HTTP 서버 없이 요청/응…
- [NestJS] Exception Filtersframeworks
- 정의 NestJS Exception Filter 는 애플리케이션에서 발생한 예외를 catch 하여 클라이언트에게 응답 형식을 통일합니다. 데코레이터로 어떤 예외를 처리할지 지정합…
💬 댓글