はじめに
TypeScriptでエラーハンドリングをするとき、 throw
を使うことがあると思いますが、throw
には見えない落とし穴があります。この記事では、throw
の問題点と、それを解決してくれるライブラリ neverthrow
について解説します。
なぜ throw
は危ないの?
一見すると、throw
を使ったエラーハンドリングはシンプルで分かりやすく見えます。
function risky(): string {
if (Math.random() > 0.5) {
throw new Error('失敗しました');
}
return '成功!';
}
しかし、この関数の戻り値の型は string
。エラーになる可能性があるにも関わらず、型情報からそれが読み取れません。
つまり、関数名や型からは、失敗する可能性があることが分かりません。
const result = risky(); // TypeScriptはstringだと認識
console.log(result.toUpperCase()); // 実行時にクラッシュの可能性
このように、throw
は TypeScript の型システムの外にあるため、型チェックで安全性を担保できません。
neverthrowとは?
neverthrow
は、関数の結果を 「成功か失敗か」 を型で表現できるようにする TypeScript ライブラリです。
Rustの Result
型にインスパイアされています。
npm install neverthrow
neverthrow
のメリット
1. 型でエラーを扱える
-
Result<T, E>
型で、成功 (Ok
) か失敗 (Err
) を明示できる。 - IDEで補完されるので、どちらのケースも漏れなく書ける。
2. try/catch が不要で読みやすい
- 関数の戻り値で処理を分岐できるため、ネストが減る。
- 失敗が予測可能になる。
3. 安全に処理をつなげられる
-
map
,mapErr
,andThen
などのメソッドを使って、成功時だけ処理を続けられる。
基本の使い方
import { Result, ok, err } from 'neverthrow';
function divide(a: number, b: number): Result<number, string> {
if (b === 0) {
return err('ゼロで割ることはできません');
}
return ok(a / b);
}
const result = divide(10, 2);
if (result.isOk()) {
console.log('成功:', result.value);
} else {
console.log('エラー:', result.error);
}
比較しながら理解する:throw
vs neverthrow
・throw
を使った場合(型システムの外)
function divide(a: number, b: number): number {
if (b === 0) {
throw new Error('ゼロで割ることはできません');
}
return a / b;
}
この関数の型は divide(a: number, b: number): number
。
でも実際には b === 0
のときに throw
が発生します。
TypeScriptの型的には "number を返す"
とされているのに、実行時には「返さない(= エラーになる)」可能性がある。
問題点:
- 呼び出し側からは 「失敗する可能性がある」ことが分からない。
- IDEの型情報では
number
が返るとしか見えない。 - catch し忘れるとアプリがクラッシュする。
・neverthrow
を使った場合(型システムの中)
import { Result, ok, err } from 'neverthrow';
function divide(a: number, b: number): Result<number, string> {
if (b === 0) {
return err('ゼロで割ることはできません');
}
return ok(a / b);
}
この関数の型は Result<number, string>
。
これは「成功すれば number、失敗すれば string(エラーメッセージ)」ということを、型が正確に表しています。
利点:
- 呼び出し側は
.isOk()
や.isErr()
で結果を安全に扱える。 - IDEでも「成功か失敗か」を明示的に意識できる。
- catch を忘れてクラッシュすることはない。
実践:複数の処理をつなげてみる
function parseNumber(s: string): Result<number, string> {
const num = Number(s);
return isNaN(num) ? err('数値ではありません') : ok(num);
}
function square(n: number): Result<number, string> {
return ok(n * n);
}
const result = parseNumber("5").andThen(square);
result.match({
ok: (v) => console.log("結果:", v),
err: (e) => console.error("エラー:", e),
});
処理の流れとしては
-
parseNumber
→ 文字列を数値に変換(失敗したらerr
) -
square
→ 数値を2乗してok
で返す -
andThen
→ 前が成功なら次の処理へ、失敗ならスキップ -
match
→ 成功時・失敗時の処理を分けて実行
実行結果の例
-
"5"
を渡す → 結果:25
-
"abc"
を渡す → エラー:数値ではありません
ポイント
-
andThen
を使うと、安全に処理をつなげられる -
match
で 成功・失敗を型で分けて処理できる -
neverthrow
の基本的な流れがこのコードに詰まってる!
おわりに
TypeScriptで安全なコードを書くためには、エラー処理を型の中に閉じ込めることがとても大切です。
neverthrow
を使えば、「エラーになる可能性」を明示的に型として扱えるので、より堅牢で読みやすいコードが書けます。