I missed Go's `if err != nil`, so I built errval for TypeScript
Go의 `if err != nil`이 그리워 TypeScript용 errval을 만들었습니다
errval은 TypeScript에서 오류를 명시적으로 반환하고, 호출부에서 처리하도록 돕는 라이브러리입니다. 오류 타입을 호출에서 추론하고 `match`의 누락된 분기를 컴파일 오류로 잡습니다. 실패 경로 벤치마크와 오류 생성 방식도 공개했습니다.
- 주제
AI 요약
Go와 TypeScript를 함께 쓰는 작성자는 Go의 명시적 오류 반환 방식을 TypeScript에서도 쓰고 싶어 errval을 만들었습니다. 의존성이 없고 압축 크기는 1.86 kB입니다. 함수는 [err, value] 튜플을 반환하며, 오류가 있으면 먼저 처리합니다. 오류 타입은 fail() 호출에서 추론하므로 직접 유니온을 선언하지 않아도 됩니다.
오류 처리와 타입 검사
const [err, user] = await getUser(id)처럼 값을 받은 뒤 오류를 확인합니다. if (err) return fail(err)를 지나면 user.email에서 값이 없을 가능성을 따로 처리하거나 !를 붙이지 않아도 됩니다. match는 오류 종류별 분기를 검사합니다. 서비스에서 새 오류 타입을 반환하면 이를 처리하는 분기를 추가하기 전까지 핸들러 코드가 컴파일되지 않습니다.
작성자는 값보다 오류를 튜플 앞에 둔 이유도 설명합니다. [value, err] 순서에서는 const [value] = save()가 컴파일되면서 오류를 조용히 버릴 수 있습니다. [err, value]에서는 첫 요소만 꺼낼 때 값이 아니라 오류를 받으므로 같은 실수를 피합니다.
벤치마크
Node 24.16에서 요청 절반이 실패하는 핸들러를 요청당 나노초 단위로 비교했습니다. neverthrow는 198 ns, errval은 226 ns, try/catch는 2,623 ns, Effect의 runSync는 3,952 ns였습니다. neverthrow보다 느린 이유로 작성자는 errval이 이름과 메시지를 갖춘 실제 Error 인스턴스를 쓰는 점을 들었습니다. 오류가 전혀 발생하지 않는 경우에는 try/catch가 195 ns, errval이 203 ns였습니다.
작성자는 속도보다 오류 유니온 추론을 만들려는 목적이 더 컸다고 밝힙니다. 오류 객체 생성에는 기본 설정에서 25 ns가 들고, Error 하위 클래스를 만드는 방식은 1,978 ns가 걸렸다고 설명합니다. 기본 경로는 Error 생성자를 실행하지 않고 프로토타입을 조정해 instanceof를 통과시킵니다. native: true는 Error.stackTraceLimit을 0으로 설정해 생성자를 실행하며, Node 24에서 164 ns입니다. stack: true는 Error.captureStackTrace를 추가해 약 2,600 ns가 걸립니다. Bun은 stackTraceLimit을 무시해 native: true에서 약 300 ns가 든다고 덧붙입니다.
dev.to 반응
- @mihai_leanzero — 질문에 바로 답하면, 오류 우선 순서가 맞습니다. 값 우선 순서는
const [value] = save()가 타입 검사를 통과해 실제 실패를 조용히 버립니다. 오류를 앞에 두면 그 방법이 막힙니다.instanceof주장도 궁금해서 소스를 살펴봤습니다.TaggedBase는new Error()를 호출하지 않고 프로토타입만 다시 연결하므로 생성자 비용 없이instanceof가 통과합니다. 서로 헷갈리기 쉬운 선택지가 두 개 있습니다.native: true는 실제Error생성자를 사용하며 문서에는 약 150ns라고 나옵니다.stack: true는 빠른 경로를 유지하면서Error.captureStackTrace를 추가하고, 같은 문서에서 몇 마이크로초가 든다고 합니다. 실제 비용이 큰 선택지는 native가 아니라 스택 캡처라고 알리면 좋겠습니다.- @aymanepraxe — 제대로 읽어주셔서 감사합니다. 소스를 직접 열어봐 주셔서 기쁩니다. 한 가지 세부사항이 말씀하신 점을 더 뒷받침합니다.
native: true가 저렴한 이유는super()호출 중Error.stackTraceLimit을 0으로 설정해 스택 캡처를 건너뛰기 때문입니다. 그게 핵심입니다. Node 24에서 기본 설정은 25ns, native는 164ns,stack: true는 약 2,600ns로 측정했습니다. 두 설정 모두에서 실제 비용은 스택 캡처이며 생성자 자체는 거의 비용이 들지 않습니다. 다만 Bun은stackTraceLimit을 무시하므로 native가 약 300ns입니다. 일반적인new Error와 거의 같은 비용입니다.
- @aymanepraxe — 제대로 읽어주셔서 감사합니다. 소스를 직접 열어봐 주셔서 기쁩니다. 한 가지 세부사항이 말씀하신 점을 더 뒷받침합니다.
- @amorizz — 벤치마크를 나눠 보여줘서 API 사용 편의성과 런타임 비용을 구분할 수 있습니다. 도입 전에 비동기 경계를 넘을 때 튜플이 어떻게 동작하는지 시험해보겠습니다. 서비스 경계에서 감쌀 때 원래 오류의 cause와 stack을 보존하나요, 아니면 별도 헬퍼가 필요한가요?
- @eva-nomados — 개인 사이드 프로젝트에서는 꼭 써보고 싶습니다. 몇 단계 아래에서 던져지는, 눈에 잘 띄지 않는 try/catch가 정말 싫습니다.
- @latrisha_5a24fb5a824484b3 — errval에서 추론되는 오류 유니온이 가장 흥미롭습니다. Go 스타일의 명시적 오류 처리를 TypeScript에 가져오면서 모든 분기를 검사하면 실패 경로를 훨씬 쉽게 이해할 수 있겠습니다. API 설계에만 집중하지 않고 벤치마크 비교로 절충점도 보여준 점이 좋습니다. 명시적 오류 처리를 선호하는 TypeScript 개발자에게 흥미로운 접근입니다. 개발 도구와 자료는 codecan.net에서도 더 찾아볼 수 있습니다.
원문: dev.to / 번역·요약: Trawling