Clojureにおける例外処理やエラーハンドリングについてまとめ.

Clojure Exception Basics

Javaの仕組みを使う. したがってJavaの知識があるとbetter.

Clojureはそもそも🔖検査例外のハンドリングが強制されない. 本当に必要なときのみに書けばいい. そのためJavaコードのようにメンテが不自由な汚いコードにならない.

Clojureの例外について書かれている記事は少ないものの, 以下の記事はけっこうな分量だけどClojureの例外の話題を網羅的にいろいろ書いてる.

Exceptions in Clojure

例外をそもそも発生させないように表明プログラミング📝Clojure specも検討.

Clojure Exception Syntax

Javaのようにtry/catch/finallyが利用できる.

(try
  (/ 2 1)
  (catch ArithmeticException e
    "divide by zero")
  (finally
    (println "cleanup")))

throw/ex-info/ex-data/ex-message

throwシンタックスで例外を引数にとり例外を発生させる.

  • 例外はJavaのクラスで作成できる(Exception. )

Javaの仕組みでなくex-infoをつかうとClojureの文法で書ける. いいかえるとMapを渡せる.

  • Clojureの記法でex-infoはmessageとmapを受取リ例外を発生させる.
  • Clojureの記法でex-dataはex-infoで入力したmapを展開する.
(try
  (throw (ex-info "bad" {:a 1 :b 2}))
  (catch clojure.lang.ExceptionInfo e
    (prn "caught" e)))

ex-messageが行メッセージ, ex-dataが詳細情報.

(try
  (let [response (http/post
                   "http://localhost:8080/v1/leads"
                   {:form-params {:foo "somethingBad"}})]
    (log/info "This is the response" response))
  (catch Exception e
    (log/error (str "Oops! " (ex-message e)))
    (log/error (str "Because! " (ex-data e))))

ex-infoで作成された例外でければJavaの例外なのでJavaの仕組みで情報にアクセスする.

  • (.getMessage ex): エラーメッセージ取得.
  • (.getCause ex): 例外クラス取得.

ref. Throwableクラスのメソッドまとめ

Clojure Stacktraces

print-stack-traceは標準出力に結果が出るので, 文字列にしてロギングしたいときは with-out-str をつかう.

(log/error with-out-str ex)

連続的なプロシージャ呼び出しのエラーハンドリングどうするか問題

Clojureでシステムの外部とやりとりをするときに,ライブラリを使って一連の動作をするシーンがよくある. このとき, 例外処理をどうするか問題. とくにRESP API呼び出しやDBとの通信などで頻発する.

(try
   (let [value (func-that-throws)]
      (act-on-value value))
   (catch Exception e
      (log/error e "func-that-throws failed")))

threading-macroをカスタムするようにRailway oriented programming?で記述するほうほうと, try-catchとifを用いてゴリゴリ処理する方法があるようだ.

ref. Clojure とエラーハンドリング at 2021 - Qiita

手続き的な処理をletにずらずら書いていくのはどうも違和感がある(個人的な感想). そうするとスレッディングマクロでbodyに書いていくスタイルはよりClojureらしくてコードの見た目はよい. しかし仕組みがdefaultでClojureに組み込まれていないので汎用的ではない. 手続きが数個に過ぎないならばtryとletとif, そうでなければrailwayを使えばいいかな.

Railway oriented programming(鉄道指向プログラミング)

鉄道指向プログラミングとは、F# で有名だったプログラミングスタイル. 関数の返り値に正常系の結果 result とそれ以外のパターン err のタプル [result, err] を想定し、実行フローの各プロセスで得られる err が nil でない時 Early-Return をかける方式. (ScalaやHaskellでも合った気がする).


さらに, Javaの関数を呼び出すときにライブラリが例外を挙げずにNullを返すときは some-> というスレッディングマクロを使う方法もClojureのガイドにあった.

ref. some-> - Threading Macros Guide

try-catch and if-let

tryとletをつかって愚直に書く. コードは汚くなるものの実装も理解もこっちは容易.

ref. 🔎if-let は処理の結果による分岐でつかう


try-letというマクロを提供するライブラリもある.

🤔そもそもConsistentを考慮してプログラミングするべきなのか

Clojureは並列プログラミングを強く意識してトランザクションという概念もあるくらいなので一連のオペレーションの途中でエラーが失敗したらその一連の処理自体そのものを失敗させるように作るべきなのかもしれない. つまりConsistent, 一貫性という概念.

slingshot: Enhanced throw and catch for Clojure

ref. GitHub - scgilardi/slingshot

Clojureは基本的にJavaの例外の仕組みを利用するが, さらにClojureに合わせてカスタマイズしたライブラリ.

たとえば clj-http では HTTP Responseが正常終了以外の場合はこのインスタンスを返すため, slingshotに合わせたエラーハンドリングが期待される.

try+thorw+ という2つのマクロを提供.

try+で囲まれたカッコ内だと, 隠しMap変数である&throw-contextが参照できる. (:throwable &throw-context)でThrowableオブジェクトが取得できる.

:object the caught object; :message the message, from .getMessage; :cause the cause, from .getCause; :stack-trace the stack trace, from .getStackTrace; :throwable the caught object;

catchは第1引数にselector, 第2引数にthrow+で投げたオブジェクトを受け取る. 第2引数は分配束縛によって分解する例をよく見る.

(catch [:type :tensor.parse/bad-tree] {:keys [tree hint]}
   (log/error "failed to parse tensor" tree "with hint" hint)
   (throw+))

predicateを関数にすればより詳細な判定がかける.

ref. Exceptions in Clojure

Clojure例外Topics

✅REPLで例外と戦う

*eとpstを覚えよう.

clojure.core/*e という変数に最後に発生したエラーが格納されている. clojure.coreなのでREPLから*eを叩けばすぐに参照できる.

clojure.repl/pstは *eのスタックトレースを出力する. またclojure.repl/root-causeはスタックトレースのrootとその階層を指定して出力.

(require '[clojure.repl :refer :all])
 
(pst)
(root-cause (pst) 3)

🎓pcoll pattern on Clojure

関数呼び出しをtry-catchで包む方法.

(defn pcall [f & args]
  (try
    [true (apply f args)]
    (catch Exception e [false e])))

ref. pcoll pattern in clojure http request

関数呼び出しのラッパー関数を作成するのはよく見かけるパターン. さらにフレームワークのミドルウェアとして組み込んだり. pcallというよりもwrapとかmiddlewareとかhandlerというキーワードとともに現れることが多いかも. pcallのprocedure callはhandler wrapper.

✅Clojureでヌルポ例外どうするか問題

Clojureは🔖動的型付け言語の力を手に入れた結果, ヌルポの呪いにかかった.

nilにアクセスしてヌルポにならない方法はこっちにまとめる.

nilのハンドリングまとめ

ヌルポが発生したときの例外処理をここでまとめる.

ヌルポが発生したら握りつぶして運用続行 or アプリ停止のどちらかが必要. ヌルポは想定外の動作なので開発中とか個人レベルのアプリならばアプリを落として調査して解消したほうがいい.

いずれにしろ, catch NullpointerEcetpionをするときはstacktraceをログするときの用途に留める(=握りつぶさない).

Clojure Error Message Catalog

ちょっと古いもののClojure Error Message CatalogというGitHub repoがある. Clojureのよくあるエラーをまとめてくれている.

https://github.com/yogthos/clojure-error-message-catalog

自分でも参考にしやすいように参考にしてwikiにまとめていきたいところだ.

🔗References