Created
June 24, 2015 13:04
-
-
Save metametadata/9f1c205b8092d7b2726f to your computer and use it in GitHub Desktop.
core.async error handling using <? macro (ClojureScript)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(ns frontend.api.core | |
(:require [ajax.core :as ajax] | |
[cljs.core.async :refer [chan put! close!]] | |
[frontend.async.core :refer [channel-error]])) | |
(defn <?GET [url] | |
"Async. Returns a maybe-channel." | |
(let [<?chan (chan)] | |
(ajax/GET url | |
{:handler #(do (put! <?chan %) | |
(close! <?chan)) | |
:error-handler #(do (put! <?chan (channel-error {:request-type :get | |
:url url | |
:details %})) | |
(close! <?chan)) | |
}) | |
<?chan)) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(ns frontend.async.core) | |
(defrecord -MaybeChannelError [data]) | |
(defn channel-error | |
"Creates error value which can be put into maybe-channel." | |
[data] | |
(-MaybeChannelError. data)) | |
(defn channel-error? | |
"Checks if the argument is a maybe-channel error." | |
[v] | |
(instance? -MaybeChannelError v)) | |
(defn channel-error-data | |
"Returns data from error value. | |
If value is not a maybe-channel error then nil is returned." | |
[e] | |
(if (channel-error? e) | |
(:data e) | |
nil)) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(ns frontend.async.macros) | |
(defmacro <? | |
"<! for maybe-channels. | |
Gets a value from the maybe-channel but if it's a channel error then throws it instead." | |
[ch] | |
`(let [v# (cljs.core.async/<! ~ch)] | |
(if (frontend.async.core/channel-error? v#) | |
(throw v#) | |
v#))) | |
(defmacro <?go | |
"The same as |go| but returns a maybe-channel. | |
If exception is caught inside the body it will be put into the returned maybe-channel. | |
If exception is a channel error it will be put as is, all other caught objects (JS allows to throw any object) | |
will be wrapped into channel error." | |
[& body] | |
`(cljs.core.async.macros/go | |
(try | |
(do ~@body) | |
(catch js/Object e# | |
(if (frontend.async.core/channel-error? e#) | |
e# | |
(frontend.async.core/channel-error e#)))))) | |
(defmacro chain | |
"Can be run only inside |go| or |<?go|. | |
Runs async operations sequentially, blocks until all operations are finished or exception is raised. | |
Returns a vector of results or throws a first channel error. | |
Operations must return maybe-channels or channels." | |
[& body] | |
(mapv #(list `<? %) body)) | |
(defmacro chain-settle | |
"Can be run only inside |go| or |<?go|. | |
Runs async operations sequentially, blocks until all operations are finished. | |
Does not shortcircuit on the first channel error so that all the operations will be run. | |
Returns a vector of results (some of which can be channel errors). | |
Operations must return maybe-channels or channels." | |
[& body] | |
(mapv #(list 'cljs.core.async/<! %) body)) | |
(defmacro <?chain | |
"Runs async operations sequentially without blocking. | |
Returns a maybe-channel which will eventually get a vector of operation results or a channel error. | |
Operations must return maybe-channels or channels." | |
[& body] | |
`(<?go (chain ~@body))) | |
(defmacro <chain-settle | |
"Runs async operations sequentially without blocking. | |
Does not shortcircuit on the first channel error so that all the operations will be run. | |
Returns a channel which will eventually get a vector of operation results (some of which can be channel errors)." | |
[& body] | |
`(cljs.core.async.macros/go (chain-settle ~@body))) | |
(defmacro all | |
"Can be run only inside |go| or |<?go|. | |
Runs async operations simultaneuosly, blocks until all operations are finished or channel error is received. | |
Returns a vector of results (order of operations is preserved) or throws a first channel error. | |
Operations must return maybe-channels or channels." | |
[& body] | |
`(let [channels# ~(vec (for [op body] op)) | |
; atom/doseq/<? is used because for/<?, mapv/<?, etc. raise "Error: <! used not in (go ...) block" | |
results# (atom [])] | |
(doseq [ch# channels#] | |
(swap! results# conj (<? ch#))) | |
@results#)) | |
(defmacro all-settle | |
"Can be run only inside |go| or |<?go|. | |
Runs async operations simultaneuosly, blocks until all operations are finished. | |
Does not shortcircuit on the first channel error so that all the operations will be run. | |
Returns a vector of results (some of which can be channel errors), order of operations is preserved. | |
Operations must return maybe-channels or channels." | |
[& body] | |
`(let [channels# ~(vec (for [op body] op)) | |
results# (atom [])] | |
(doseq [ch# channels#] | |
(swap! results# conj (cljs.core.async/<! ch#))) | |
@results#)) | |
(defmacro <?all | |
"Runs async operations simultaneuosly without blocking. | |
Returns a maybe-channel which will eventually get a vector of results (order of operations is preserved) or a channel error. | |
Operations must return maybe-channels or channels." | |
[& body] | |
`(<?go (all ~@body))) | |
(defmacro <all-settle | |
"Runs async operations simultaneuosly without blocking. | |
Does not shortcircuit on the first channel error so that all the operations will be run. | |
Returns a channel which will eventually get a vector of results (some of which can be channel errors), order of operations is preserved. | |
Operations must return maybe-channels or channels." | |
[& body] | |
`(cljs.core.async.macros/go (all-settle ~@body))) | |
" | |
Other exercises: | |
- implement the same functionality as functions instead of macros if possible, it may require changing signature, i.e.: | |
before: | |
(<?chain | |
(op1 1 2 3) | |
(op2 4 5 6)) | |
after: | |
(<?chain | |
#(op1 1 2 3) | |
#(op2 4 5 6)) | |
- alt/alts/first-like macro for maybe-channels which can throw exception if selected value is an error | |
- chain-like macro which returns all the values until first error + the error | |
" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(ns frontend.views.file.controller | |
(:require [frontend.views.file.view :as view] | |
[frontend.api.core :as api] | |
[frontend.router.core :as router] | |
[cljs.core.async :refer [chan put! <!]] | |
[frontend.async.core :refer [channel-error? channel-error-data]]) | |
(:require-macros [cljs.core.async.macros :refer [go]] | |
[frontend.async.macros :refer [<? <?go | |
chain chain-settle | |
<?chain <chain-settle | |
all all-settle | |
<?all <all-settle]])) | |
(defn- <?load-files! [state] | |
(println " load-files!") | |
(<?go | |
(swap! state assoc :files-loading? true) | |
(swap! state assoc | |
:files (<? (api/<?GET "api/files/")) | |
:files-loading? false) | |
"<load-files result for debugging>")) | |
(defn- <?initialize-state! [state] | |
(println " >>> initialize-state!") | |
(swap! state merge {:files nil | |
:files-loading? nil | |
:current-file-id nil}) | |
(<?load-files! state)) | |
(defn- <?load-current-file-content! [state] | |
(<?go | |
(let [file-id (:current-file-id @state)] | |
(println " load-current-file-content!" file-id) | |
(when (not= file-id :none-file-id) | |
(swap! state assoc-in [:files file-id :loading?] true) | |
(swap! state assoc-in [:files file-id :content] (:items (<? (api/<?GET (str "api/files/" file-id))))) | |
(swap! state assoc-in [:files file-id :loading?] false)) | |
"<load-current-file result for debugging>"))) | |
(defn- <?set-current-file! [state file-id router] | |
(println " >>> set-current-file!" file-id) | |
; first make sure that file id exists | |
(let [file-id (if (contains? (:files @state) file-id) | |
file-id | |
:none-file-id)] | |
(router/push-tag router | |
(if (= file-id :none-file-id) :file-none :file) | |
{:file-id file-id}) | |
(swap! state assoc :current-file-id file-id) | |
(<?load-current-file-content! state))) | |
(defrecord Controller [state router] | |
view/ControllerProtocol | |
(on-navigation [_ file-id] | |
(println "controller: on-navigation" file-id) | |
(go | |
(try | |
(chain | |
(<?initialize-state! state) | |
(<?set-current-file! state file-id router)) | |
(catch js/Object e | |
(println "!!! ERROR - on-navigation - caught" | |
(if (channel-error? e) | |
(str "error from some channel, error data: " (channel-error-data e)) | |
(str "js exception: " e))))))) | |
(on-change-current-file [_ file-id] | |
(println "controller: on-change-current-file" file-id) | |
(go | |
(try | |
; <? is needed to wait for result or raise an exception | |
(<? (<?set-current-file! state file-id router)) | |
(catch js/Object e | |
(.error js/console "on-change-current-file exception:" e)))))) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
My try at implementing core.async error handling in ClojureScript as described in article by David Nolen:
http://swannodette.github.io/2013/08/31/asynchronous-error-handling/
Usage example is demonstrated in files:
frontend-api-core.cljs
- shows how to create a "maybe-channel" for AJAX GET requestfrontend-views-file-controller.cljs
- seeController
record at the bottom of the file, it's methodon-navigation
is intended to be called when user enters the page andon-change-current-file
will be triggered if user changes "current file" by using UI menu.There's no built-in way to propagate errors in core.async. And one of the solutions is to manually pass special channel error values:
Based on these functions new macros are implemented:
<?
is the same as<!
but throws an exception if the incoming value is a channel error instance.<?go
is the same asgo
but also catches all errors in body and wraps them into channel error value to be put into returned channel.Also implemented are macros for coordinating several async operations (see docstrings for more info).
Naming convention is taken from the style guide by Eric Normand:
http://www.lispcast.com/core-async-code-style
Specifically prefix
<
is used for functions which return channels and<?
is used for functions returning "maybe-channels" (i.e. the ones which can contain channel error values). The same prefixes are used for channel var names.Other useful links:
https://github.com/alexanderkiel/async-error
http://martintrojer.github.io/clojure/2014/03/09/working-with-coreasync-exceptions-in-go-blocks/
http://wyegelwel.github.io/Error-Handling-with-Clojure-Async/
https://github.com/kachayev/async-errors
https://gist.github.com/ericnormand/6009721
https://gist.github.com/swannodette/6385166
https://github.com/fullcontact/full.async
https://github.com/funcool/promesa
https://github.com/jamesmacaulay/cljs-promises
http://clojurescriptmadeeasy.com/blog/promises-with-core-async.html
http://blog.venanti.us/using-transducers-with-core-async-clojurescript/