🍊

【TypeScript】クロージャを使ってエラーハンドリングを共通にする

に公開2

本記事は、リファクタリングの際にクロージャ(二重関数呼び出し)を使ったエラーハンドリングの共通化を調べた備忘録です。

TL;DR

クロージャのメリット

  • エラーハンドリングを共通化・柔軟化できる
  • 処理を一元管理でき保守性が向上する

クロージャのデメリット

  • クロージャの理解が必要になる
  • 小規模のコードでは過剰になる

修正前コード

エラーの発生した文脈以外は共通する実装が並んでいます。これではコードの見通しが悪くなり、他の処理を追加するときには似たコードを繰り返し書く必要がありますし、エラーメッセージを修正したいときにはすべての箇所が対象になります。

// 処理A
await clearTask(context).catch((err) => {
    console.error("タスクの削除中にエラーが発生しました:", err);
});

// 処理B
await doneTask(taskId,context).catch((err) => {
    console.error("タスク完了中にエラーが発生しました:", err);
});

// 処理C
await listTasks(taskId, context).catch((err) => {
    console.error("タスク一覧取得中にエラーが発生しました:", err);
});

修正後コード

handleError に文脈(タスクの削除、タスク完了etc)を渡すだけになりました。これで処理の追加やエラーメッセージの修正が簡単になります。

// 処理A
await clearTask(context).catch(handleError("タスクの削除"));

// 処理B
await doneTask(taskId,context).catch(handleError("タスク完了"));

// 処理C
await listTasks(taskId, context).catch(handleError("タスク一覧取得"));

// クロージャを使う
function handleError(context: string) {
  return (err: unknown) => {
    console.error(`${context}中にエラーが発生しました:`, err);
  };
}

クロージャを使った理由

次の共通関数の例の通り、今回の実装ではそこまで実装量は減りません。それでもクロージャにしたのは実践の意味合いが強いです。

クロージャを使わずに共通関数を採用する例

クロージャを使わずに同様の処理を実装するには共通関数でも実装できます。

// 処理A
await clearTask(context).catch((err) => handleError(err, "タスクの削除"));

// 処理B
await doneTask(taskId, context).catch((err) => handleError(err, "タスク完了"));

// 処理C
await listTasks(taskId, context).catch((err) => handleError(err, "タスク一覧取得"));

// クロージャではない共通関数
function handleError(err: unknown, context: string) {
  console.error(`${context}中にエラーが発生しました:`, err);
}

両者の比較

クロージャ(二重関数呼び出し) 単純な共通関数
書き方 .catch(handleError("文脈")) .catch((err) => handleError(err, "文脈"))
文脈を外側で注入 可能 可能(ただし呼び出し側で書く必要)
共通処理の一元管理 内側関数にまとめる 共通関数でまとめる
コードの短さ やや短い やや長い(無名関数を書くぶんだけ)
可読性 関数の関数の理解が必要 単純で直感的
拡張性・柔軟性 高い(ログ以外の処理を追加しやすい) 中くらい(handleError に処理を書くと肥大化しやすい)

.catch で戻り値を制御するには

.catch(handleError("タスク追加")) は一見短く書けますが、戻り値を返さないので後続チェーンで undefined になります。戻り値を明示するには次のように書きます。

).catch((err) => {
  handleError("タスク追加")(err);
  return 1;
})

{} ブロック内の (err) に驚いたためにこの記事を書きました。

設計上の注意

  • 小規模コードではやりすぎになりやすい
  • テストを書くときに文脈の関数を別途用意する必要が出る

Discussion

junerjuner

クロージャというよりも bind で束縛した感じの……カリー化に近い感じでしょうか?

1
めんだこめんだこ

コメントありがとうございます!
おっしゃる通りで、確かに「bindで引数を先に束縛」や「部分適用・カリー化」と非常に近い発想だと思います。
今回は context をスコープに閉じ込めるクロージャの形で書きましたが、bind を使えば同じように context を固定した関数を返すこともできますし、構造としてはほとんど同じですね。
パターン的には部分適用やカリー化と説明する方が正確かもしれません。

1