dev.to

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: trueError.stackTraceLimit을 0으로 설정해 생성자를 실행하며, Node 24에서 164 ns입니다. stack: trueError.captureStackTrace를 추가해 약 2,600 ns가 걸립니다. Bun은 stackTraceLimit을 무시해 native: true에서 약 300 ns가 든다고 덧붙입니다.

dev.to 반응

  • @mihai_leanzero — 질문에 바로 답하면, 오류 우선 순서가 맞습니다. 값 우선 순서는 const [value] = save()가 타입 검사를 통과해 실제 실패를 조용히 버립니다. 오류를 앞에 두면 그 방법이 막힙니다. instanceof 주장도 궁금해서 소스를 살펴봤습니다. TaggedBasenew 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와 거의 같은 비용입니다.
  • @amorizz — 벤치마크를 나눠 보여줘서 API 사용 편의성과 런타임 비용을 구분할 수 있습니다. 도입 전에 비동기 경계를 넘을 때 튜플이 어떻게 동작하는지 시험해보겠습니다. 서비스 경계에서 감쌀 때 원래 오류의 cause와 stack을 보존하나요, 아니면 별도 헬퍼가 필요한가요?
  • @eva-nomados — 개인 사이드 프로젝트에서는 꼭 써보고 싶습니다. 몇 단계 아래에서 던져지는, 눈에 잘 띄지 않는 try/catch가 정말 싫습니다.
  • @latrisha_5a24fb5a824484b3 — errval에서 추론되는 오류 유니온이 가장 흥미롭습니다. Go 스타일의 명시적 오류 처리를 TypeScript에 가져오면서 모든 분기를 검사하면 실패 경로를 훨씬 쉽게 이해할 수 있겠습니다. API 설계에만 집중하지 않고 벤치마크 비교로 절충점도 보여준 점이 좋습니다. 명시적 오류 처리를 선호하는 TypeScript 개발자에게 흥미로운 접근입니다. 개발 도구와 자료는 codecan.net에서도 더 찾아볼 수 있습니다.

원문: dev.to / 번역·요약: Trawling