Rust의 Result 타입으로 Node.js 에러 핸들링 개선하기
문제 상황
결제 API 서버를 운영하면서 에러 핸들링이 복잡해지는 문제를 겪었다. try-catch가 중첩되고, 어디서 어떤 에러가 발생할지 타입 레벨에서 알 수 없었다.
try {
const user = await getUser(userId);
try {
const payment = await processPayment(user);
return payment;
} catch (e) {
// 결제 실패 처리
}
} catch (e) {
// 유저 조회 실패 처리
}
Rust의 Result 패턴
Rust를 학습하며 Result<T, E> 타입을 알게 됐다. 성공(Ok)과 실패(Err)를 명시적으로 표현하는 방식이 인상적이었다.
TypeScript로 간단히 구현해봤다.
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
function Ok<T>(value: T): Result<T, never> {
return { ok: true, value };
}
function Err<E>(error: E): Result<never, E> {
return { ok: false, error };
}
적용 사례
async function getUser(id: string): Promise<Result<User, 'NOT_FOUND' | 'DB_ERROR'>> {
const user = await db.user.findUnique({ where: { id } });
if (!user) return Err('NOT_FOUND');
return Ok(user);
}
async function processPayment(userId: string) {
const userResult = await getUser(userId);
if (!userResult.ok) {
if (userResult.error === 'NOT_FOUND') {
return Err('USER_NOT_FOUND');
}
return Err('DB_ERROR');
}
const user = userResult.value; // 타입 안전
// 결제 처리
}
결과
- 함수 시그니처만 봐도 어떤 에러가 발생할 수 있는지 명확함
- try-catch 중첩 제거
- 타입스크립트의 union type과 잘 맞아떨어짐
당장 모든 코드를 바꾸진 않았지만, 중요한 비즈니스 로직부터 적용 중이다. Rust를 배우며 얻은 가장 실용적인 인사이트였다.