Одной из основных претензий к Java у меня всегда была её “многословность”. Особенно эта многословность раздражает в обработке исключений, где сравнительно небольшой участок кода часто обильно сдабривался “соплями” обработки исключений. Разнообразные среды типа IDEA эту многословность позволяли в той или иной степени сгладить в процессе написания кода, но, к сожалению, она никуда не девалась из кода уже написанного и мозолила глаза. Как эту радость прятали в Scala я уже не помню, т.к. язык мне показался мало пригодным для моих нужд, а вот варианты доступные в Clojure мне очень даже понравились.
Исключения в Clojure, по логике работы с ними, можно разделить на две группы. Первая группа – это исключения возбуждаемые Java-библиотеками. Для примера возьмем функцию выполняющую запрос HEAD:
(client/head url)
)
Само собой, эта функция выкинет Java-исключения, если запрашиваемый хост будет не найден, что является типичным для Java-библиотек. Стандартный подход к обработке таких исключений так же ничем не будет отличаться от того, что все привыкли видеть в Java коде. Всё те же развесистые обработчики увесистой иерархии исключений, которые только портят читабельность кода:
(try
(test-fn "https://qqq.www/")
(catch java.io.IOException e
(log/info "try/catch :("))))
Ко второй группе исключений относятся Clojure-специфические исключения не имеющие никакой дополнительной иерархии и реализуемые классом
(try
(throw (ex-info "My text" {:type :my-error1 :cause :something-happens}))
(catch clojure.lang.ExceptionInfo err ;; (1)
(let [data (ex-data err)]
(case (:type data) ;; (2)
:my-error1 (prn "Error 1 handler" (:cause data))
:my-error2 (prn "Error 2 handler" (:cause data))
)))))
Ловится исключение с указанием типа объекта
При этом, в Clojure мире, имеется ощутимо более удачный вариант реализуемый библиотекой slingshot. Сама slingshot работает поверх стандартных исключений Clojure с использованием
(try+ ;; (1)
(throw+ {:type :my-error1 :cause :something-happens}) ;; (2)
(catch [:type :my-error1] {:keys [type cause]} ;; (3)
(prn "Error 1 handler" cause))
(catch [:type :my-error2] {:keys [type cause]}
(prn "Error 2 handler" cause))
(catch Object err ;; (4)
(prn "all other errors" (ex-data err)))
)
)
Изменения привносимые slingshot в обработку исключений я бы назвал косметическими, при этом нельзя не отметить что данный синтаксический сахар очень и очень удобен.
Но, честно говоря, не это привлекло меня в работе с исключениями в Clojure, а библиотека dire. Для примера возьмем приведенную выше функцию
(try
(prn "BEGIN :: test-dire")
(prn "DATA ::" (test-fn "https://goo.gl/404"))
(prn "END :: test-dire")
(catch Exception e
(log/info "try/catch :(" (type e)))))
В консоли окажется вполне ожидаемый текст и на первый взгляд можно перехватить именно
=> nil
Aug 13, 2015 9:56:12 PM error-handling.test invoke
INFO: try/catch :( clojure.lang.ExceptionInfo
[/cc]
На практике все немного сложнее, для того что бы в этом убедиться достаточно заменить https://goo.gl/404 на https://qqq.www/404:
=> nil
Aug 13, 2015 10:00:03 PM error-handling.test invoke
INFO: try/catch :( java.net.UnknownHostException
Как я и говорил выше, “прилететь” может не только Clojure-специфическое
(try
(prn "BEGIN :: test-dire")
(prn "DATA ::" (test-fn "https://goo.gl/404"))
(prn "END :: test-dire")
(catch clojure.lang.ExceptionInfo e ;; (1)
(let [data (ex-data e)
status (get-in data [:object :status])
url (get-in data [:environment 'req :url])] ;; (2)
(log/info "HTTP(S) code" status "for URL" url)))
(catch Exception e ;; (3)
(log/info "try/catch :(" (type e)))))
Не очень красиво, правда? Зато можем извлечь потенциально полезную информацию о деталях ошибки и записать её в удобной форме в лог
[:status *]
(fn [e & args]
(let [{:keys [status request-time]} e]
(log/info "with-handler: status" status "for" (first args) "with request time" request-time))
))
(with-handler! #'test-fn ;; (2)
java.io.IOException
(fn [e & args]
(log/info "with-handler/Object" (.getMessage e))
))
Благодаря dire к одной функции можно прикрепить цепочку обработчиков, которые будут вызываться последовательно в порядке объявления. В примере выше сначала
Кроме явного улучшения читабельности кода, имеется еще один крайне важный побочный эффект: так как исключение уже было обработано во внешнем обработчике, функция
"DATA ::" nil
"END :: test-dire"
=> nil
Aug 13, 2015 10:03:41 PM error-handling.test invoke
INFO: with-handler: status 404 for https://goo.gl/404 with request time 254
Возможно, я упустил что-то в вопросе работы с исключениями в Clojure, и я был бы очень благодарен за дополнения.