概要
- neverthrow を理解したいのにドキュメントが難しい、そんな人向けに「例外処理ってそもそも何?」からじっくり噛み砕いていく記事です
- 抽象概念が苦手な人でも、ストーリーで理解できる構成になってるはず、です
経緯
- 非同期処理の壁――それは、一度は立ちはだかる壁
- 理解を深めるために格闘していた最中、非同期処理の相棒・例外処理こと「エラーハンドリング」のライブラリ・neverthrow と出会う
- が、何を読んでも概念と使い方がピンとこない!
- と、いうわけで納得するまで AI (gemini) を質問攻めにした結果をシェアハピしていきます(⌒∇⌒)
書いてる人
-
認知特性がゴリゴリのファンタジー型(映像・言語優位)
- 脳内処理では受け取った情報をイメージや映像に変換して処理する
- ⇒ 変換できない抽象概念、ちんぷんかんぷん!
- 最近は遭遇してイメージできない抽象概念を AI に質問攻めにしている
- 添えるプロンプトは 「小学 6 年生でも分かるように、丁寧に分かりやすく日本語で説明して」(重要)
この記事
- 超長いです
- 図も多めにしてます
- 斜め読みで必要なところを探してください
- 次章の「ざっくり目次」で概ねたどり着けると思います
ざっくり目次
- 1. 例外と例外処理について
- 2. 例外処理と非同期処理について
- 3.
neverthrowの深堀 - 4. 同期処理の書き方比較(try/catch, neverthrow)(※本題1)
- 5. 非同期処理の書き方比較(try/catch, async/await, neverthrow)(※本題2)
ざっくりショートサマリー:「例外」「非同期」「neverthrow」の関係ざっくり図解
| 概念 | 一言でいうと | neverthrow で何が変わる? |
|---|---|---|
| 例外処理 ( try/catch) |
「例外が起きたら止める」仕組み |
Result型でパニックを起こさず結果を返せる |
| 非同期処理 ( async/await) |
「結果が出るまで待つ」仕組み |
ResultAsync型で成功・失敗を型安全に管理できる |
neverthrow |
「例外を“普通の結果”として扱う」ライブラリ | 例外をthrowせず、成功も失敗も同じように扱える |
neverthrow って?
- TypeScript 用のエラーハンドリングのライブラリ
javascript では使えないの?
-
neverthrowの最大にして本質が「型安全」の担保 - ⇒ 型を重視しない js では本領発揮できない
neverthrowって、どんな動作?
- 例外処理を使わずにエラーの処理を行う
- エラーが発生したら「エラーが起きたよ」という結果を返すだけ(処理の強制終了がない)
- if 文でエラーの有無と中断ポイントを作れる!
- 成功時とエラー時の処理の書き漏らしが減る
イメージ図
そもそも「例外」ってなに? ~neverthrowに進む前に~
以下、AI の回答を人間に読みやすく再構築してます
例外(Exception)=パニックになる大声
イメージ:たくさんのブロックで、すごく大きなタワーを作っている最中
-
「次のブロックをちょうだい」
- 指示:友達(次のプログラムの部品)に「はい、次は青いブロックをちょうだい!」と頼む
-
トラブル発生!
- 友達が青いブロックを持ってない!
-
大声で泣く ⇒ 例外発生の合図!
- 友達は「うわあああ!青いブロックがないよー!うわ~~ん!」と、突然、大声を出して泣き出しちゃった!
-
泣いてパニック=プログラムが強制終了……
- 友達の大声を聞いた瞬間、あなた(プログラム)はビックリして、持っていたブロックを全部落としちゃった
-
タワー作りはすぐに中断
- 「次はどうする?」という予定も、全部吹き飛んじゃった
-
タワーを完成させるには?
- 大声(友達)を誰かが捕まる
- 大声(友達)「よしよし、大丈夫だよ」と慰める
- 最初からやり直す
つまり?
- プログラミングで例外(大声)が起こる
- その場所でプログラムは強制停止
- その後の予定(プログラム)は全部キャンセル!困っちゃうね!
イメージ
今後の説明では例外発生のことを「大声を出す」「パニック」などの表現も交えていくよ!
じゃあ 例外「処理」って?
※AI の回答を(略)
- 「もしパニック(以降強制終了)になる大声(例外)が出ても、慌てずに、決まった手順で対応する」ための、プログラマーが作る 「安全な計画」 のこと。
例外処理(安全な計画)の作り方
- この処理は例外が出るかも!⇒ 安全網を用意しておこう
- ベーシックな例外処理:
try/catch記法
- ベーシックな例外処理:
-
try:試す- 「この部分を試してみて。ただし、大声が出るかもしれないよ」と、危ない作業を安全な箱に入れる処理
-
catch(e):捕獲 - 「もし try の安全箱の中で大声が出たら、必ずこの耳(catch)で捕まえて、ここで静かに対応する」という対応チーム(捕獲要員)を用意する処理
-
(e)はcatchに入る引数のこと
-
なんで「安全」になるの?
- パニック(強制終了)が広がらない
- 大声(例外)が出ても、
catchの対応チームがすぐにその声を捕まえてくれる- ⇒ タワー作り全体がパニック(強制終了)になるのを防止できる
- 大声(例外)が出ても、
- 次工程に進める!
-
catchの中で「今回は失敗したけど、大丈夫。代わりにメッセージを出して、次の作業(別の遊び)に進もう」という指示が出せる
-
混同ポイント:非同期処理と例外処理はズッ友だよ!
Q. 一緒に
Promiseやらasyncやらawaitやら見かけるけど、あれは例外処理とは関係ないの?A.
Promiseやasync/awaitは「非同期処理」と言って、例外処理ではないよ!
でも、いつも一緒に使われるから、同じプログラム内で見かけることが多いよ。
例外処理と非同期処理はズッ友だよ!
非同期処理って? 例外処理との違いは?
- このあと
neverthrowの使い方でたくさん出てくるから、この機会に整理しよう! - 例として「お菓子を買うお使いに行く」工程で説明するよ!
※AI(略)
-
非同期処理とは?
-
「時間のずれ」を扱う道具 - 「時間がかかる作業を、他の作業を止めずに進める」 ための道具
-
「いつ発生したか」が焦点
項目 役割 非同期処理 お菓子屋さんが遠いので、結果が出るまで時間がかかる作業 Promise「必ずお菓子を買うか、買えない理由を持って帰るよ!」という約束 async/awaitお母さんが「約束の結果が出るまで、他のことをしながら待っている」ことを
わかりやすく書くための魔法の言葉。
-
-
例外処理とは?
-
「トラブル(失敗)が起きたらどうするか」 を決める計画
-
「何が起きたか」が焦点
項目 役割 例外 お菓子屋さんが急に「キャー!お菓子売り切れ!」と
パニック(処理停止)になる大声(例外)を出すこと例外処理 「大声が出たときに、プログラム全体が止まらないように、
大声を捕まえて対応する安全ネット(try/catch)を作ること。
-
-
なんでズッ友なの?
- 「時間がかかるお使い(非同期処理)」は「トラブル(例外)が起きやすい」から!
- 非同期処理:遠いお菓子屋さんまで行く ⇒ 時間がかかる
- 例外:行っている間にお菓子が売り切れるかもしれない ⇒ お使い中に発生し得るトラブル
- 「トラブルが起きそうな、時間のかかるお使い(非同期処理)」
- ⇒「トラブルシューティング(例外処理)」も必須!
小括
- 非同期処理: 「待つ」ための道具
- 例外処理: 「失敗に対処する」ための道具
- 「待つ必要があって、失敗しやすい作業」で使うのでニコイチ!
AI 公式ドキュメントの要約 ~neverthrowに話を戻す~
- インストール@
package.jsonと同じディレクトリ$ npm install neverthrow
- 安全に利用するために専用の ESLint プラグインも導入
$ npm install eslint-plugin-neverthrow
Q. neverthrow の公式ドキュメント記載の各機能を、小学 6 年生でもわかるように丁寧にわかりやすく日本語で説明して
A. 「おつかい」に例えて説明していくよ!
- Neverthrow:安全な「結果の箱」を返すライブラリ
-
基本の道具:
Result型 ⇒ 結果の箱箱の種類 プログラム上の名前 中身のイメージ 意味 成功 Ok<T>欲しかったお菓子(=データ= <T>)おつかい大成功 ☆ 失敗 Err<E>失敗報告書(=エラー= <E>)失敗の詳細報告 プログラムは「成功したのか、失敗したのか」が型(箱)を見ただけで必ずわかる!
-
魔法の道具:結果の箱の中身を取り出す・加工する ⇒ 箱を開けずに中身を安全に扱う
メソッド 実行 機能 イメージ 実行条件と逆の時 map成功 中身を加工 箱の中のお菓子だけを、「もっと美味しい味」に変身させる魔法 失敗報告書が入っていたら、そのまま次へ渡す mapErr失敗 失敗報告書を書き直す 「もっとわかりやすい理由」に書き直して、新しい箱に入れる魔法 箱の中に「お菓子」が入っていたら、何もせずそのまま次の箱に渡す andThen成功 次のおつかいを連続で始める 箱の中に「お菓子」がある ⇒ そのお菓子を使って次の新しいおつかい(次の Result を返す関数)をスタート 失敗の箱が届いたら、次のドアは開かず、おつかい全体がそこでストップ match両方 必ず両方の処理を書いて、一つの結果にまとめる 成功と失敗の両方のレシピを必ず書く
⇒「成功(Ok)」と「失敗(Err)」で、最後に一つのお菓子(結果)を作り出す
※片方でも未記述 ⇒TypeScript が指摘 orElse失敗 別のおつかいを試す 最初の箱に「失敗報告書(Err)」があったら、「よし、じゃあ次はこっちの店に行こう!」 と、別の新しいおつかいをスタートさせる 最初の箱の中に「お菓子」が入っていたら、新しいおつかいは試さず、そのまま最初の箱を渡す unwrapOr失敗 代わりの品を準備 「もし失敗報告書(Err)だったら、代わりにあらかじめ用意しておいたオモチャを出すよ。成功(Ok)だったら、そのままお菓子を出すよ」と、必ずどちらか一つの値を取り出す 最初の箱の中に「お菓子」が入っていたら、「代わりのオモチャ」は無視して、中のお菓子(値)をそのまま取り出す safeUnwrap両方 絶対成功すると信じて開ける 「絶対にお菓子が入っているはずだ!」と強く信じて、力ずくで箱を開ける道具。成功したら、すぐにお菓子が取り出せる もし失敗報告書が入っていたら、「ええっ!」とパニックになる大声(例外) を出してプログラムが止まってしまう
※危険魔法 -
時間がかかるおつかい(非同期処理)用の道具
⇒Promise,async/awaitなど、結果が出るのに時間がかかるおつかいの処理が得意!メソッド 機能 イメージ ResultAsync「まだ終わっていないおつかい」 の結果が入る予定の箱 時間がかかる処理が終わる ⇒ 最終的に、中には OkかErrが入っているokAsync成功した箱を、非同期で作る 成功のお菓子(Ok)を、「今すぐ」 用意して、運送業者(Promise)に渡してすぐ出発させる魔法 errAsync失敗した箱を、非同期で作る 失敗報告書(Err)を、「今すぐ」 用意して、運送業者(Promise)に渡してすぐ出発させる fromPromise「大声を出す Promise」を「ResultAsync の箱」に変身従来の「大声を出すおつかい」を、静かな「失敗報告書(Err)」を返すおつかいに変身させる、魔法の変換機 fromSafePromise失敗時に例外を出さない Promise を、安全な ResultAsyncに変身**「失敗しても絶対に大声を出さない」**と約束された結果を、安全な ResultAsyncの箱に詰めて運ぶための道具 -
非同期の連続処理と便利な処理
⇒ 複雑な流れ(連続したおつかい)をスムーズにし、複数の結果を賢くまとめる
メソッド 実行 機能 イメージ 実行条件と逆の時 asyncAndThen成功 次の時間がかかる処理へ繋げる 最初のお菓子が成功(Ok)だったら、「よし、じゃあ次は遠い場所にある新しいケーキ屋さんに頼もう!」と、次の時間がかかるおつかいに繋ぐ 失敗報告書(Err)だったら、次のケーキ屋へは行かず、その失敗報告書をそのまま渡す combine複数の成功 まとめて処理 牛乳、お菓子、パンの全ての箱が成功(Ok) だった場合だけ、「全部揃ったよ!」という一つの大きな箱にまとめてくれる(Promise.all が使われていた場面で使用) 一つでも失敗報告書があったら、他の成功を無視して最初に起きた失敗をすぐに渡す combineWithAllErrors複数の失敗 まとめて報告 牛乳、お菓子、パンを頼んで、失敗報告書だけを全部集めてくれる道具。「牛乳は売り切れ、パンは焦げていた」のように、起きた全ての失敗理由を全て纏めて一つの大きな報告書にして渡してくれる(Promise.all が使われていた場面で使用) 失敗が一つもなければ、「エラーはなかったよ」という空の成功の箱を返す -
危ない処理を安全に包む道具
⇒「例外(大声)」を出すかもしれない危ない作業を、最初からneverthrowの安全な箱に包んでくれる道具メソッド 機能 イメージ 実行条件と逆の時 fromThrowable大声を出すかもしれない関数を、安全な箱に変身 「このお店の人は失敗すると大声を出すぞ」と分かっているときに、大声を出された(失敗)場合は、自動的に失敗報告書(Err)に変わった箱を持ち帰る(関数全体を箱に詰める) 大声が出なければ、お菓子(成功)を入れて箱を渡す fromSafePromise失敗時に例外を出さない Promise を、安全な箱に変身 「絶対に大声を出さない店」 におつかいに行って、失敗報告書(Err)かお菓子(Ok)のどちらかが安全に入った箱を持ち帰る もともと失敗しても大声は出ないため、安全に成功または失敗が箱に入る safeTryコードの実行を試し、大声が出たら箱に詰める 「ちょっと試したい危ない実験」 をした結果、成功したらその結果をお菓子(Ok)に、失敗して大声が出たら、捕まえて失敗報告書(Err)にしたコードを箱に入れて持ち帰る 大声が出なければ、コードの結果をそのままお菓子(成功)として箱に入れる プログラムを動かす前に、あらかじめ「パニックになる可能性のある場所」に予防線を張るための大切な役割
同期処理のフロー(Result 型使用)
非同期処理のフロー(ResultAsync 型と async/await を使用)
AI に聞いてみた:neverthrow の理解向上・対話まとめ
- neverthrow に対する、AI とのやり取りのプロンプトと回答を丸っと貼っています
- 回答が結構長いので折りたたんでます
全て「小学 6 年生でもわかるようにわかりやすく丁寧に説明してほしい」と添えて質問投下
1. そもそも Neverthrow って?
Q. Neverthrow ライブラリ自体について
A.neverthrow ライブラリがどんな道具なのか、小学 6 年生のみんなにも分かるように、丁寧にお話ししますね。
neverthrowは、「失敗しても大声(例外)を出さない、とても安全なエラー処理の道具」 です。これを理解するために、「失敗を伝える2つの方法」で比べてみましょう。
🛑 1. 今までの失敗の伝え方(例外と大声)
今までのプログラミング(JavaScript や TypeScript)では、おつかい(関数)が失敗すると、こんなことが起きていました。
例:おつかいでお菓子が買えなかった
- 「キャー!😢」と大声を出す (
throw new Error(...))
- お店の人が、買えない理由を説明する代わりに、突然「キャー!お菓子が売り切れ!」と大声を出します。
- 周りがパニックになる
- この大声を聞くと、そのおつかいを頼んだ人(呼び出し元のプログラム)はパニックになって、その後の仕事が全部ストップしてしまいます。
- 大声を捕まえるのが大変
- 大声(例外)は、遠くまで飛んでいくので、それを聞くための「
try...catch」という特別な耳(リスナー) を用意しないと、どこで何が起きたか分からなくなります。このように、大声(例外)で失敗を伝える方法は、パニックを引き起こし、流れを止め、どこで何が起きたか分かりにくくするという欠点がありました。
✅ 2.
neverthrowの失敗の伝え方(結果の箱)
neverthrowを使うと、失敗を伝える方法が変わります。これは、「結果を必ず箱に入れて渡す」 というルールです。例:Neverthrow でお菓子が買えなかった!
neverthrowのおつかいは、どんなときでも必ず「結果の箱」を持って帰ります。
箱の名前 中身 意味 失敗してもパニックにならない理由 Ok 🥛 成功したお菓子(買えたもの) 大成功! 欲しかったデータが手に入った。 - Err 📝 お菓子が買えない失敗報告書 失敗した! 失敗した理由が詳しく書いてある。 大声ではない! 箱の中身なので、静かに受け取って確認できる。
neverthrowが安全な理由
- 大声を出さない: 失敗しても「失敗報告書」を箱に入れて返すので、誰もパニックになりません。
- 見逃せない: プログラムは、この箱を受け取ったら、
matchやif文を使って「Ok のときはどうする? Err のときはどうする?」と必ず確認するルールになります。確認しないと TypeScript の宿題チェックに怒られます。- 流れがスムーズ: 失敗した理由(報告書)は、次の人(次の関数)にそのまま静かにバトンタッチできるので、処理の流れが途中で止まることがありません。
魔法の道具:ResultAsync と fromPromise
そして
neverthrowには、時間がかかるおつかい(Promise)を、この安全な箱に自動で変身させるfromPromiseという特別な魔法の道具があります。これのおかげで、難しい非同期処理も簡単に安全に扱えるようになるんです。
🌟 まとめ
neverthrowは、「すべての結果(成功・失敗)を型(箱)で表現する」 ことで、プログラム全体をパニックやバグから守るための、とても賢くて安全な道具なんですよ!Neverthrow を使うと、あなたは
- 失敗の理由を絶対に見逃さなくなります。
- 大声を出すコードを書かなくて済むようになります。
2. async / await 構文での使用について
Q. Promise ではなく async / await 構文で非同期処理を行なっている TypeScript で neverthrow へのリライトを検討している。
その場合、async / await 構文自体を取りやめて neverthrow の構文に書き直すことになるのか?A. いえ、
async/await構文を取りやめる必要はありません!
むしろ、
async/awaitはneverthrowを使う上でとても相性が良いんです。小学 6 年生の皆にもわかるように、Promise と Neverthrow、
async/awaitの関係を「おつかい」に例えて説明しますね🚀
async/awaitとneverthrowの関係(おつかい編)1. いつもの「おつかい」(Promise/async/await)
もし、お母さんから「おつかいに行って、帰ってきたら結果を教えてね」と頼まれたとします。これがプログラミングでいう「非同期処理」です。
項目 説明 失敗の伝え方 Promise おつかいの約束です。成功したらお菓子、失敗したら泣き顔のスタンプがもらえる約束。 泣き顔スタンプ( reject/throw) を渡す。async/awaitこの「約束」が守られるまで、お母さんが待っていること( await)を、わかりやすく書くための魔法の言葉です。失敗したら、お母さんは「キャー!😢」と大声(例外) をあげて知らせます。 今までのコードは、お母さんが「キャー!😢」という大声で失敗を知らされるやり方でした。
2.
neverthrowを導入した「新しいおつかい」ここで
neverthrow(「絶対に大声を出さない」)を導入すると、ルールが変わります。
neverthrowのおつかいは、結果を「お菓子(成功)」 または 「エラー報告書(失敗)」 のどちらか一つの箱に入れて持ち帰ることがルールです。
項目 説明 失敗の伝え方 Result 型 お菓子かエラー報告書のどちらか一つが入った箱(Result)です。 エラー報告書( Err) を箱に入れて渡す。async/awaitお母さんが結果を待つ魔法の言葉はそのまま使えます! 失敗しても、お母さんは大声を出さず、箱の中のエラー報告書を見て失敗を知ります。 大事なポイント
neverthrowにリライトするときに書き換えるのは、「失敗の伝え方」 の部分だけです。
- 書き換え前(例外を投げる)
if (失敗) {
throw new Error('お店が閉まっていた!'); // ❌ 大声(例外)を上げる
}
- 書き換え後(Result を返す)
if (失敗) {
return err({ message: 'お店が閉まっていた!' }); // ✅ エラー報告書を箱に入れて返す
}
「おつかいが終わるまで待つ」という**
async/awaitの役割は、結果の箱が届くまで待つという動作にそのまま使えるため、async/await構文は活かしながら**、中身の失敗の伝え方だけをneverthrowのルール(Result型)に書き直すことになるんですよ!結論
neverthrowを使うことで、関数は必ずPromise<Result<成功, 失敗>>のような型を返すようになります。
async💡 → 使います(Promise を返すため)await💡 → 使います(結果が返ってくるのを待つため)
neverthrowは、非同期処理をより安全でわかりやすくするためのライブラリなんです。
3. neverthrow の非同期処理メソッドと、async / await 構文について
Q. ResultAsync、fromPromise があるなら、async/await 構文の前に元の Promise 構文のままでも良いのでは?
その上で async/await 構文と neverthrow の相性がいい理由は?A.
neverthrowとasync/awaitの関係は、実はプログラミングの中でも少し難しいところですが、じっくり、丁寧に説明しますね。
結論から言うと、
async/await構文はそのまま使って大丈夫です。そして、使った方が圧倒的に便利なので、ぜひ使いましょう!Promise、
async/await、neverthrowの関係を、「回転寿司で注文した料理が届くプロセス」 に例えて説明します。
1:元の Promise 構文(
.then())の難しさ元の Promise 構文(
.then()や.catch()をつなげる書き方)は、回転寿司の**「高速で流れるベルトコンベア」**のようなものです。問題点:ベルトが速すぎる
あなたは板前さんに「アジ、マグロ、ウニの順でお願いします」と注文しました。
- アジの皿(Result の箱)がベルトで流れてきます。
- 「Ok(成功)かな? Err(失敗)かな?」と見て、次のマグロの皿を注文する処理(
.then())をすぐに書かなければなりません。- もしアジの皿に「Err(骨が入っていた報告書)」が入っていたら、あなたは全てのベルトを止めて、代わりに「イクラを頼む」という複雑な処理(
.catch()や.orElse())を前の段階で準備しておく必要があります。
.then()構文は、処理を事前に全て予約しておき、流れる皿に反射的に反応するような書き方になります。これが何段階も続くと、頭の中でベルトコンベアの流れを追いかけるのがとても大変になってしまうのです。
2:
async/awaitとneverthrowの相性が良い理由ここで、
async/awaitの「一時停止ボタン」と、neverthrowの「安全な箱」が組み合わさります。1.
async/awaitの役割は「待つ」こと
async/awaitが変えるのは、「失敗の伝え方」ではなく、「結果が返るのをどう待つか」 という書き方だけです。
awaitは、お皿があなたの席の前を通るときに「ちょっと待って!」と一時的にベルトを止める魔法の言葉です。2. 相性の良さ:立ち止まって箱の中身を確認できる
neverthrowのお皿(Result)は、必ず「Ok」か「Err」のラベルが付いた箱です。
async/awaitのおかげでベルトが止まっていると、あなたは落ち着いて次の行動が取れます。
構文 動作 例(イメージ) await皿(結果の箱)を目の前で一時停止させる。 「よし、アジのお皿が来た。ちょっと待て!」 neverthrow皿に「Ok」か「Err」のラベルが貼ってある。 「この皿、青いラベル(Err)が貼ってあるぞ!」 if文ラベルを見て、次に何をすべきかをすぐに決める。 「もし青いラベルだったら、マグロの注文は中止して、すぐにイクラを頼もう」 と、その場で簡単に判断できます。 3. コードが「普通のレシピ」になる
async/awaitを使うと、コードを上から下へ、まるで普通の料理のレシピのように書けます。
Promise ( .then())async/await+neverthrow複雑 シンプル .then((result) => { ... })const result = await getSushiAsync();.catch((error) => { ... })if (result.isErr()) { return; }
async/awaitのおかげで、あなたは「箱が届くのを待って、届いたらすぐに箱のラベルをチェックする」という、人間にとって最も自然な流れでエラー処理を書けるようになるんです。これが、
neverthrowの「安全な箱」を「一時停止ボタン」を使って一つ一つチェックできるasync/await構文と組み合わせるのが、最も強力で分かりやすい理由です。
4. 可読性について
Q. ややこしい Promise を resultasync、from Promise を使って処理するよりも、async/await 構文で最後に neverthrow の match 機能を使ってリターンする方が可読性が上がるしコーディングも楽、という認識で合ってる?
A. はい、その認識は概ね合っています!素晴らしい理解力です。
特に「可読性が上がる」「コーディングが楽」という点では、async/awaitを使うのが今の TypeScript で最も推奨される方法です。
なぜそうなるのかを、回転寿司からバトンタッチして「おつかいの道順」の例でじっくり説明しますね。
1:Promise チェーンは「迷路の地図」
ResultAsyncやfromPromiseを使い、.andThen()や.map()などのメソッドを繋げていく書き方(これをメソッドチェーンと呼びます)は、「迷路の地図」のようなものです。❌ 従来の Promise チェーン(迷路の地図)のイメージ
- 「まずお菓子屋さんに行きます」
.andThen→ 「成功したら、次にお菓子屋さんに行くための約束をします」.andThen→ 「成功したら、次にパン屋さんに行くための約束をします」この書き方は、成功と失敗の道筋をすべて事前に地図に書き込んでおく必要があります。
- もし途中で失敗(Err)が起きても、どの道筋で失敗したのかを上に戻って確認しなければならず、処理の流れが行ったり来たりして、コードが「迷路」のように見えてしまいます。
- 処理を止めたい(早期リターンしたい)ときも、Promise のルールに合わせて複雑な書き方をしなければなりません。
2:
async/awaitは「一本道」で立ち止まれる
async/awaitを使うと、コードは「一本道」になります。そして、neverthrowの箱(Result)が届くたびに、立ち止まって中身をチェックできます。✅
async/awaitのメリット(一本道の確認)
async/awaitでの書き方は、あなたの認識の通り、次のようになります。
const お菓子の箱 = await お菓子屋に行く();
(一時停止ボタン:お菓子屋からの結果が届くまで待つ)if (お菓子の箱.isErr()) { return お菓子の箱; }
(箱を開けてチェック:「もしお菓子が買えてなかったら、これ以上進まずに家に帰る」)const お菓子の箱 = await お菓子屋に行く();
(一時停止ボタン:お菓子屋からの結果が届くまで待つ)if (お菓子の箱.isErr()) { return お菓子の箱; }
(箱を開けてチェック:「もしお菓子が買えてなかったら、これ以上進まずに家に帰る」)なぜ可読性が上がるのか?
- 上から下に流れる: 処理が上から下へ、時間通りに流れるので、頭の中で迷子になりません。
- 早期リターンが簡単: 失敗したときに、複雑な
.catch()を書かずに、return(すぐに家に帰る)という、人間が考えるのと同じ自然な動作で、その後の処理を全てストップできます。- if 文でシンプルにチェック:
if文は誰でも知っている簡単なルールです。この簡単なルールで、複雑なエラー処理ができてしまうので、コーディングがとても楽になります。
結論:あなたの認識は正しい!
「
async/await構文で書いて、途中でResultをチェックしながら処理を進める」という方法は、可読性、保守性、コーディングの楽さのすべてにおいて、従来の Promise チェーンよりも優れています。Promise チェーンを使うのは、小さな処理をポンと繋げたいときや、どうしても
.thenの書き方が好きな人が使うくらいだと思って大丈夫です。補足:「最後に
match」について
match機能は非常に強力ですが、多くの場合は処理の途中でif (result.isErr()) return result;のように早期リターン(途中で帰宅)するために使われます。
matchは、「成功したとき(Ok)と失敗したとき(Err)で、最終的に何か違うことをする」ときに使うと、もっと便利になりますよ!
5. match の意義
Q. 「エラーが起きたらエラー内容の詳細を表示する」ような処理を挟むなら match を使う意義がある?
A. 「エラー内容の詳細を表示する」という処理を挟む場合、match 機能は使う価値が大いにあります。
ただし、
async/awaitと組み合わせた場合、いつ、なぜmatchを使うと便利なのかを理解するのが重要です。あなたと私で、
matchが得意なことを「プレゼントを渡す方法」に例えてじっくり見ていきましょう。
🎁 パート1:「if 文」は、失敗したら「すぐに帰る」方法
前回説明した
async/awaitの中でif (result.isErr())を使う方法は、「途中で問題があったら、すぐにその場を離れて、そこで処理をストップする」のが得意です。
if (isErr()) returnの流れ(すぐに帰宅)
- プレゼントの箱が届いた (
await)if (箱がErrだったら)- 友達に「ごめんね、プレゼント壊れてた(エラー内容)」と伝えて表示する。
return(すぐに家に帰る/後の処理を全てやめる)。この方法(if 文+早期リターン)は、複数の手順があるとき(おつかいのステップなど)に、処理を止めたいときに一番簡単で、可読性も高いです。
🎭 パート2:「match」は、「成功と失敗、どちらも同じゴールに着地する」方法
では、
matchが得意なのはどんなときでしょうか?それは、「成功しても失敗しても、最後に必ず何か一つの結果を返す」というルールがある場合です。
matchの得意なこと(一つのゴール)
matchは、次のような状況で「絶対にどちらのパターンも書き忘れない」ように強制してくれます。例:ウェブサイトに表示するメッセージを決める
あなたがウェブサイトの担当者で、ユーザーに「何かメッセージ」を必ず表示しなければならないとします。
- データ取得の結果が、プレゼントの箱で届いた。
- ここで
matchを使います。
match の場合 処理 意味 ok:の中成功したデータを使って、「✨ 購入完了しました!」というメッセージを作る。 成功したら、このメッセージがゴールの結果になる。 err:の中失敗の理由(詳細)を使って、「申し訳ありません。エラーコード XX で失敗しました」というメッセージを作る。 失敗しても、このメッセージがゴールの結果になる。 ご覧の通り、
matchを使うと、成功と失敗のどちらを通っても、最後に必ず「メッセージ」という一つの結果になります。「エラーを表示する」処理で
matchを使う意義あなたが求めている「エラーが起きたらエラー内容の詳細を表示する」という処理で
matchを使うと、次のような意義があります。
意義 説明(なぜ matchが優れているか)書き忘れ防止 matchを使うと、「成功したとき(Ok)」の処理を必ず書くことになります。これにより、「エラーの表示は書いたけど、成功したときに何も起こらない」というミスを防げます。統一された出力 成功データ(T)と失敗データ(E)という全く違う二つの型から、「画面に表示する HTML や文字列」という一つの型(A)に変換する作業が、とてもシンプルで安全に書けます。 🌟 結論:使い分けのルール
使い方 if (isErr()) return(早期リターン)match得意なこと 途中の処理で失敗が確定したら、そこで処理をストップして、すぐに親元の関数にエラーを返す(エラーの伝言ゲームを続ける)。 成功と失敗を最後の段階で一つにまとめ、最終的な表示や戻り値という「結果」を必ず作る。 適している場所 複数のステップがある処理の途中(例:認証 → データ取得 → データ保存) 最終的にユーザーに何かを表示する直前や、関数が単一の戻り値を返す直前 あなたがもし「エラーを表示したら、もうそれ以上は何もしたくない」という場合は
if (isErr())が楽です。しかし、「エラーでも、エラーメッセージという形で最終的な結果を必ず画面に出したい」という場合は、
matchが最も安全で分かりやすい方法になります!
結局、具体的にどう使うの? どうリライトするの?
- 正直一番知りたいのって、使い方だよね!
- ドキュメントや記事を読んでもピンとこなかったので、簡単な関数で、基本的な使い方を整理しました(⌒∇⌒)
- 複雑な使い方は今回は端に寄せます
- サンプルコード+図解のセットでお送りします
1. 同期処理:try/catch と neverthrow 比較
try/catch(従来の処理)
const tryCatch = (arg: string) => {
// 従来のtry/catchで処理
try {
// 意図的にエラーを発生させる
if (arg === 'throw') {
throw new Error(`引数 "${arg}" のため大声(例外)発生`);
}
// ✅成功時の処理 (try ブロック)
console.log(`[Try/Catch] 成功: 引数 ${arg}`);
} catch (error) {
// ❌エラー発生時 (catch ブロック)
console.error(`[Try/Catch] エラー:引数 ${arg} 処理失敗`, error);
}
};
// 親関数で呼び出し
tryCatch('test'); //✅実行結果:[Try/Catch] 成功: 引数 test
tryCatch('throw'); //❌実行結果:[Try/Catch] エラー:引数 throw 処理失敗", 引数 "throw" のため大声(例外)発生
neverthrowにリライト
- ファイル頭に
import { fromThrowable, Result } from 'neverthrow';でライブラリを読み込み - 「メインの処理」「安全変換」「呼び出し」の3段構造
- 使用するメソッド
-
Ok<T>:成功したときの箱。T は中身(お菓子) -
Err<E>:失敗した時の箱。E は中身のエラー(失敗報告書) -
match:成功と失敗の両方のレシピを必ず書 -
fromThrowable:大声()例外を出すかもしれない関数を、安全な箱に変身させる -
Result:結果の箱
-
- 実行したい根幹の処理(メイン処理)
- エラーを発生させる可能性のある「純粋な処理」関数
- この関数は大声(例外)を throw する可能性があるため、危ない。
const dangerousProcess = (arg: string): string => {
if (arg === 'throw') {
// ❌例外発生!
throw new Error(`引数 "${arg}" のため大声(例外)発生`);
}
return `処理されたデータ: ${arg}`;
};
- 危険な処理を安全箱に入れる(convert to safeBox)
-
fromThrowableを使用して危ない処理を安全なResultの箱に変換 -
⇒fromThrowableが例外を静かに捕まえ、Errの箱に変換 -
checkNeverthrow()自体はdangerousProcess()の「監視役」の関数 -
return fromThrowable(危険関数実行+エラー処理)();でfromThrowableを実行+戻す
-
/**
* @param arg テスト引数
* @returns Result<string, Error>
*/
const checkNeverthrow = (arg: string): Result<string, Error> => {
// fromThrowableで dangerousProcess をラップし、例外(throw)を Err の箱に変換する
return fromThrowable(
// 試したい危ない関数dangerousProcessを呼び出し
() => dangerousProcess(arg),
// ❌dangerousProcessで大声(例外)が出たら、それをErrの箱に入れる
// Err<Error>⇒「失敗報告書」
// checkNeverthrow:Result<string, Error>のErrorの方に格納される
(error) => error as Error
)(); // ラップした関数を実行"();← この () が肝!"
};
- 呼び出すための関数(fainal:caller)
-
Resultを受け取り、matchで成功/失敗の処理を分岐 - ⇒ 元の try/catch の出力の役割と同じ!
-
/**
* @param arg テスト引数
* @returns void(returnは無い)
*/
const callNeverthrowProcess = (arg: string): void => {
// dangerousProcessの実行結果を受け取ってresultに格納
const result: Result<string, Error> = checkNeverthrow(arg);
// match を使って、成功/失敗の両方のパターンを確実に処理する
result.match(
(value) => {
// ✅Ok (成功) の処理
// Ok内のデータ(お菓子)が自動的にvalueに格納されている
console.log(`[Neverthrow] 成功: ${arg} | データ: ${value}`);
},
(error) => {
// ❌Err (失敗) の処理:失敗報告書
console.error(`[Neverthrow] エラー: ${arg} 失敗報告書`, error);
}
);
};
- 実行結果
callNeverthrowProcess('test');
// ✅[Neverthrow] 成功: test | データ: 処理されたデータ: test
callNeverthrowProcess('throw');
// ❌[Neverthrow] エラー: throw 失敗報告書 Error: 引数 "throw" のため大声(例外)発生
2. 非同期処理:Promise / try/catch /async/await / neverthrow
- 一番使う(重要)
try/catch(基礎的な処理)
-
.thenで繋いでいく一番基本の Promise チェーン-
.catch()がtry/catchのcatch と同等の役割を担っている
-
- 非同期処理として
fetch使用-
Promise:fetch以下の処理が終わるまで待機 -
resolve:✅ 成功 -
reject:❌ 失敗
-
-
res.json():「受け取ったオブジェクトの JSON 解析が完了し、JavaScript オブジェクトが準備できたら、そのオブジェクトを結果として渡す」 という約束 (Promise) を返す
/**
* @param url フェッチ対象のURL
* @returns Promise<void>
*/
const tryCatchPromise = (url: string): Promise<void> => {
// fetch() は Promise<Response> を返す
return (
fetch(url)
// 結果をResponse型変数resに格納
.then((res: Response) => {
// ✅成功時: HTTPステータスコードをチェックし、問題があれば手動で例外を投げる
// ⚠️HTTPステータスコードのチェック
if (!res.ok) {
// Promiseチェーン⇒この中での「throw」は、次の.catch()ブロックに渡される
throw new Error(`HTTPエラー: ${res.status}`); // ❌catch()ブロックへ
}
// JSON形式に変換する処理(res.json():Promiseを返す)
// Promiseチェーンを継続するため、次の.then()に返す
return res.json();
})
// 変換されたjsonを変数dataに格納
.then((data) => {
// 成功時: データを取得し、ログを出力
// このブロックは、fetch()とres.json()の両方が成功した場合に実行
console.log(`[Try/Catch Promise] 成功: ${data.name}`);
})
.catch((error) => {
// ❌: Promiseチェーンのどこかで発生したエラー(throwまたはreject)を捕獲
// ネットワークエラー、HTTPエラー(手動throw)、JSON解析エラーなどはここに集約
console.error(`[Try/Catch Promise] エラー:`, error);
})
);
};
// ----------------------------------------------------------------------
// 実行例
// ----------------------------------------------------------------------
// ✅成功パターン (有効なAPIを使用する場合)
tryCatchPromise('https://pokeapi.co/api/v2/pokemon/1');
// 結果:[Try/Catch Promise] 成功: bulbasaur
// ❌失敗パターン (存在しないエンドポイント)
tryCatchPromise('https://pokeapi.co/api/v2/pokemon/9999');
// 結果:[Try/Catch Promise] エラー: Error: HTTPエラー: 404
// ❌失敗パターン (無効なURLやネットワークエラー)
tryCatchPromise('https://invalid-domain-name-xyz.com');
// 結果:[Try/Catch Promise] エラー: TypeError: Failed to fetch
// 結果:[Try/Catch Promise] エラー: TypeError: Load failed
// 結果:[Try/Catch Promise] エラー: Error: Network request failed
// ※環境に依存
try/catch+async/await(より簡潔な処理)
-
async/awaitを基本として、安全処理をtry/catchで行う書き方 - Promise チェーンの .catch() ⇔ 処理全体を try/catch
- 非同期処理として
fetch使用 - 同期処理的な見た目で非同期処理を行う
/**
* @param url フェッチ対象のURL
* @returns Promise<void>
*/
const asyncAwaitProcess = async (url: string): Promise<void> => {
try {
// 1. fetchの完了を待つ
// ⇒Promiseチェーンの最初の .then() の前
const res: Response = await fetch(url);
// 2. ⚠️HTTPステータスコードのチェック
// Promiseチェーンの .then() 内部の
// if (!res.ok) { throw... } と同じ処理。
if (!res.ok) {
// ❌ここでthrow ⇒即座に catch にジャンプします。
throw new Error(`HTTPエラー: ${res.status}`);
}
// 3. ✅JSON解析の完了を待つ
// ⇒Promiseチェーンの2番目の .then() の前
const data: Promise = await res.json();
// 4. ✅成功時の処理
console.log(`[Try/Catch Async] 成功: ${data.name}`);
} catch (error) {
// ❌エラー発生
// catch ブロックは、Promiseチェーンの .catch() と同様に、
// ネットワークエラー、手動で throw されたエラー、JSON解析エラーのすべてを捕獲
console.error(`[Try/Catch Async] エラー:`, error);
}
};
// ----------------------------------------------------------------------
// 実行例
// ----------------------------------------------------------------------
// ✅成功パターン (有効なAPIを使用する場合)
asyncAwaitProcess('https://pokeapi.co/api/v2/pokemon/1');
// 結果:[Try/Catch Async] 成功: bulbasaur
// ❌失敗パターン (存在しないエンドポイント)
asyncAwaitProcess('https://pokeapi.co/api/v2/pokemon/9999');
// 結果:[Try/Catch Async] エラー: Error: HTTPエラー: 404
- 全体的な流れは Promise チェーンと同じ!
neverthrow
- 今回の!本題!!
- 使用・関連する neverthrow メソッド
-
Result:結果の箱 -
ResultAsync<T,E>:「これから実行するお使い」の結果が入る予定の箱<成功,失敗> -
Ok:ResultAsyncが成功したときの中身(お菓子) -
Err:ResultAsyncが失敗した時の中身(失敗報告書) -
fromPromise:「大声(例外)を出すPromise」を「ResultAsyncの箱」に変身させる -
andThen:次のおつかいを続けて開始 -
map:箱の中のお菓子だけを、「もっと美味しい味」に変身させる魔法
-
- 以下、ライブラリインポート+ユーザー定義型
// neverthrowライブラリを読み込み
import { Result, ResultAsync, ok, err, fromPromise } from 'neverthrow';
// --- エラーと成功の型を明確に定義 ---
// ✅成功時のデータ型(今回ではJSONの一般的なオブジェクト)
type SuccessData = {
name: string;
};
// ❌失敗時のエラー型 (Neverthrowでは失敗の型Eの厳密な定義を推奨)
// ユニオン型が含まれているので型エイリアスとして定義
type FetchError = {
type: 'HTTP_ERROR' | 'NETWORK_ERROR' | 'PARSE_ERROR';
message: string;
status?: number; // HTTPエラーの場合のみ
};
- メイン処理の詳細
- ✅ 成功時は
SuccessData - ❌ 失敗時は
FetchError- それぞれ
ResultAsyncの箱で返す
- それぞれ
- 同期処理の工程の 1(危険処理)と 2(安全化)を合体させたイメージ
-
try/catchとの差:各エラーセクションで詳細なエラーを格納できる
- ✅ 成功時は
/**
* @param url 取得対象のURL
* @returns ResultAsync<SuccessData, FetchError>
*/
const convertNeverthrow = (url: string): ResultAsync<SuccessData, FetchError> => {
// 1. fetch処理を「大声(例外)を出す Promise」として fromPromise でラップ
// fromPromiseは「ResultAsyncの箱」に変身させる道具なので、戻り値の型はResultAsync
// 成功:✅Response≒fetchのresolve
// 失敗:❌FetchError≒fetchのreject
const fetchPromise: ResultAsync<Response, FetchError> = fromPromise(
fetch(url), // fetchは成功時にPromise<Response>を返す
(error): FetchError => ({
// errorは暗黙的にunknownと推察される
// ❌ Promiseが reject (ネットワークエラーなど) されたとき、
// その例外(Error)を Err の失敗報告書(FetchError)に変換する
type: 'NETWORK_ERROR',
message: `ネットワーク接続に失敗: ${(error as Error).message}`,
// ここではerrorがunknownのままだとプロパティが使用できない
// ⇒型アサーションでError型に定義
})
);
// 2. ✅andThen⇒Responseが成功(Ok)した場合の次の処理を継続する
return (
fetchPromise
// fetch成功時のResponseオブジェクトを変数resに格納
// Response型には要素としてok,statusが含まれている
.andThen((res: Response) => {
// 2-1. HTTPステータスコードのチェック
// 従来のtry内のステータス確認の工程
if (!res.ok) {
// ❌ res.okがfalseの場合、throwではなく Err の箱を返す
// 「Err の箱」=FetchError型の変数⇒失敗報告書
const httpError: FetchError = {
type: 'HTTP_ERROR',
message: `HTTPエラーが発生: ${res.status}`,
status: res.status,
};
// ResultAsync内で失敗報告書を返す
return err(httpError);
// errAsync() でも可:ResultAsync<never, FetchError>を即座に返す
}
// 2-2. JSON解析処理も fromPromise で安全にラップする
return fromPromise(
res.json() as Promise<SuccessData>, // jsonの型であるPromiseの詳細型に、事前定義の成功型を指定
// ❌ JSON解析エラーが発生した場合、それを失敗報告書(変数error)に変換
(error): FetchError => ({
type: 'PARSE_ERROR',
message: `JSON解析エラー: ${(error as Error).message}`,
})
);
})
// 3. ✅成功が確定したら、最終的な処理結果を map で加工する
// SuccessData型に変換したjson()を変数dataに入れる
.map((data: SuccessData) => {
// 成功が確定した後のデータ処理(元のコードの console.log の代替)
// 呼び出し元で match を使ってログ出力を行うため、ここではデータをそのまま返す
console.log(`[Neverthrow] 処理パイプラインを成功裏に完了。`);
return data;
})
);
};
- 非同期処理を呼び出す
- 同期処理の際の 3(呼び出し)の工程
- match で成功・失敗の処理を両方書く
-
try/catchで囲む必要がない!
-
// convertNeverthrow()を呼び出す
// 非同期処理が含まれるのでasync/awaitを使う
async function main() {
console.log('--- Neverthrow リライト後の実行 ---');
// 呼び出し処理
const successResult: Result<SuccessData, FetchError> = await convertNeverthrow('APIのURL');
// matchで成功・失敗の分岐処理
successResult.match(
(datadata: SuccessData) => {
console.log(`[結果] 成功: ${data.name} のデータを取得しました。`);
},
// ✅[結果] 成功: ** のデータを取得しました。
(error: FetchError) => {
console.error(`[結果] 失敗報告書: ${error.type} - ${error.message}`);
}
// ❌[結果] 失敗報告書: HTTP_ERROR - HTTPエラーが発生: 404(例)
// ❌[結果] 失敗報告書: NETWORK_ERROR - ネットワーク接続に失敗: Failed to fetch(例)
);
}
// 非同期処理を実行
main();
総括
- 長らく苦手意識を抱いていた非同期処理とエラー処理のコンビへの理解が多少深まったと思う。
- 実践で設計・コーディングするハードルが下がったように感じる
- 非同期処理の比較コードの分析
-
neverthrow使用時のコードは単純なasync/await構文より長くなってい - その分エラー対応が堅牢!
- ⇒ メリットの方が大きい!
-
- もう後続のライブラリが出てる!
-
neverthrowは現時点でも安定して利用されている。がしかしbyethrow のように、よりシンプルかつ使いやすさを重視した派生ライブラリも出てきている。
- 技術の進化速度、日進月歩越えて音速。
-
- AI も使い方次第。
- 抽象概念の具体化などは公式ドキュメントはもちろん、テックブログ系でもなかなかお目にかかれない
- ⇒ 対話しながら輪郭を掴んでいけるのは、ファンタジー認知型の人間にとってはありがたい。
- 技術の理解はもちろん、考え方そのものの再構築にもつながったと思う。
- mermaid 記法での図解の叩き台をサクっと作ってくれるの超助かる
- 叩き台を基に見て更に理解しやすいように微調整はした