2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

TypeScriptで安全なエラーハンドリングを実現する 『neverthrow』

Posted at

はじめに

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),
});

処理の流れとしては

  1. parseNumber → 文字列を数値に変換(失敗したら err
  2. square → 数値を2乗して ok で返す
  3. andThen → 前が成功なら次の処理へ、失敗ならスキップ
  4. match → 成功時・失敗時の処理を分けて実行

実行結果の例

  • "5" を渡す → 結果: 25
  • "abc" を渡す → エラー: 数値ではありません

ポイント

  • andThen を使うと、安全に処理をつなげられる
  • match成功・失敗を型で分けて処理できる
  • neverthrow の基本的な流れがこのコードに詰まってる!

おわりに

TypeScriptで安全なコードを書くためには、エラー処理を型の中に閉じ込めることがとても大切です。
neverthrow を使えば、「エラーになる可能性」を明示的に型として扱えるので、より堅牢で読みやすいコードが書けます。

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?