oracle-samples/clara-rules

User-generated exceptions thrown from within RHS not surfacing in CLJ

Closed this issue · 5 comments

Precept's insert! wrapper now throws an error when the user attempts to insert contradicting facts logically. I have it working in CLJS, but in Clojure I think Clara catches it and producing a different error in response.

So far I haven't been able to figure out how to throw the error in such a way that Clara does not catch the Exception. I believe the relevant line is https://github.com/cerner/clara-rules/blob/master/src/main/clojure/clara/rules/engine.cljc#L1771.

Any help on how I might be able to work around this would be appreciated. :)

The intent here is to wrap exceptions thrown in a rule RHS in an ExceptionInfo exception that contains information about the environment in which they were thrown (which rule, what facts caused the rule to fire, etc.) The original exception is added as the cause on the ExceptionInfo though so it should be accessible; see the three-argument doc for ex-info. That cause is then accessible via the getCause method.

If I'm misunderstanding, could you give a concrete example of the problem you're running into?

Sure np sorry for not being specific enough.

Using this inside RHS. In this example I expect the error to be thrown.

(defn insert!
  "Insert facts logically within rule context"
  [facts]
  (let [[to-insert to-retract] (conform-insertions-and-retractions! facts)]
    (trace "[insert!] : inserting " to-insert)
    (trace "[insert!] : conflicting " to-retract)
    (if (empty? to-retract)
      (cr/insert-all! to-insert)
      (throw (ex-info "Conflicting logical fact. You may have rules whose conditions are not
        mutually exclusive that insert! the same e-a consequence. The conditions for logically
        inserting an e-a pair must be exclusive if the attribute is one-to-one. If you have two
        identical accumulators and you are seeing this error, create a separate rule that inserts a
        fact with the accumulator's result and replace the duplicate accumulators with expressions
        that match on that fact."
                 {:arguments facts
                  :attempted-insert to-insert
                  :blocking-fact to-retract})))))))
(rule hey-there
  [[?e :entry/title ?v]]
  =>
  (println "?" ?e)
  (util/insert! [?e :entry/new-title "Hello again!"]))

Instead I get the Clara error.

Loading test/cljc/precept/rules_debug.cljc... 
? 1
CompilerException clojure.lang.ExceptionInfo: Exception in precept.todomvc.rules-debug/hey-there with bindings {:?e 1, :?v "Second"} {:bindings {:?e 1, :?v "Second"}, :name "precept.todomvc.rules-debug/hey-there", :rhs (do (println "?" ?e) (insert! [?e :entry/new-title "Hello again!"])), :batched-logical-insertions [], :batched-unconditional-insertions [], :batched-rhs-retractions [], :listeners []}, compiling:(/Users/alexdixon/precept/test/cljc/precept/rules_debug.cljc:119:1) 

When I replace the throw in util/insert! with a log, I just see the log. So it seems like Clara is catching the thrown exception and throwing a new exception in its place.

@alex-dixon I may be misreading or misunderstanding a part of what you are saying, but it sounds like Clara is still throwing an exception, it's just an exception that "wraps" your exception.

If that is the case, I'd say that is fairly typical behavior as far as how exceptions go. Any time you are throwing an exception from some other lib's code/context, I don't think it should be assumed that your exception will "bubble out" without any decoration/wrapping.

Typically you catch and rethrow exceptions with more context-specific info, which is what Clara is doing with RHS exceptions. Again, perhaps I'm just completely misunderstanding the issue though.

I think Clara should (and I believe it is) keep a reference to your exception on the cause-stack though. You could write or use something that walks the stack to find the exception you are looking for though. It typically just means walking the cause-chain. You certainly can do that in clj. In cljs I think we'd have to be sure the original error is attached in the ex-data of the exception wrapper. Hmm.

Ok. I don't think either of you are misunderstanding at all. I'm new to writing libraries and Java also. If there's a way to surface the error I'm trying to throw then there's no issue here, just wanted to make sure it was possible before going down that path. I'll try figuring out a solution that incorporates what you suggest. Thanks!

@alex-dixon no problem. Just another note - much if not most of the JVM-world tooling I've seen will print the exception chain in one form or another, not just its "end". So if your functional need is just for your specific exception to be printed when an error is thrown you may not actually need to do any specific handling. This is tool-dependent though. By the chain I mean something like E has E1 as its cause and E1 has E2 as its cause, so tooling could print something like

E 
stack trace
caused by E1
stack trace
caused by E2
stack trace