Error Handling
Strict Mode
Pathom 3 is strict by default. It's an all-or-nothing mode, or Pathom will fulfill the full query, or it will throw an exception.
Here are the reasons that may cause Pathom to throw:
- Attribute doesn't exist in the indexes
- Attribute exists in the indexes, but the available data isn't enough to reach it
- Attribute wasn't in the final output after all the processes
- A resolver threw an exception*
- An exception to this rule is if a resolver is contained in a OR node, if there are multiple options to fetch some attribute. It only throws when every option fails.
Let's see one example of each of those cases:
; Attribute doesn't exist in the indexes
(p.eql/process
(pci/register
(pbir/constantly-resolver :a "foo"))
[:b])
; => EXCEPTION: Pathom can't find a path for the following elements in the query: [:b] at path []
; Attribute exists in the indexes, but the available data isn't enough to reach it
(p.eql/process
(pci/register
(pbir/single-attr-resolver :a :b "foo"))
[:b])
; => EXCEPTION: Pathom can't find a path for the following elements in the query: [:b] at path []
; An extended version of this is also true in case of nested inputs, if Pathom can't see
; a path for a nested input requirement, it will consider it a plan failure.
(p.eql/process
(pci/register
[(pco/resolver 'nested-provider
{::pco/output [{:a [:b]}]}
(fn [_ _]
{:a {:b 1}}))
(pco/resolver 'nested-requires
{::pco/input [{:a [:c]}]
::pco/output [:d]}
(fn [_ _]
{:d 10}))])
[:d])
; => EXCEPTION: Pathom can't find a path for the following elements in the query: [:c] at path [:a]
; Attribute wasn't in the final output after all the processes
(p.eql/process
(pci/register
(pco/resolver 'data
{::pco/output [:a :b :c]}
(fn [_ _]
{:a 10})))
[:c])
; => EXCEPTION: Required attributes missing: [:c] at path []
; A resolver threw an exception
(p.eql/process
(pci/register
(pco/resolver 'error
{::pco/output [:error]}
(fn [_ _]
(throw (ex-info "Deu ruim." {})))))
[:error])
; => EXCEPTION: Deu ruim.
This mode is good when:
- You run Pathom and use the results in-process (vs over the wire)
- Your queries are small
- You want all-or-nothing from the output
Optionality
In the same way that resolvers support optional inputs, you can use the same syntax to express optional attributes in your client query.
This will ignore problems to fetch that attribute, except in case of an exception. For example:
(p.eql/process
(pci/register
(pco/resolver 'data
{::pco/output [:a :b :c]}
(fn [_ _]
{:a 10})))
[:a (pco/? :c) (pco/? :not-on-index)])
; => {:a 10} - no exception, ignore the absence of :c and :not-on-index
; exceptions still get out
(p.eql/process
(pci/register
(pco/resolver 'error
{::pco/output [:error]}
(fn [_ _]
(throw (ex-info "Deu ruim." {})))))
[(pco/? :error)])
; => EXCEPTION: Deu ruim.
Lenient Mode
When your Pathom scenario involves large queries (for a complex UI made with Fulcro for example) you probably don't want that a single attribute break the whole query.
This requires a more tolerant mode, which is the lenient mode in Pathom 3.
In lenient mode, errors are handled per attribute. The following sections will explain how to handle errors in this way.
Example setup using lenient mode:
(def env
(-> {:com.wsscode.pathom3.error/lenient-mode? true}
(pci/register ...)))
Attribute Errors
Pathom is an abstraction around attributes, and the error handling follow this idea by thinking of errors as things that happen per attribute.
This is counter-intuitive from new people using Pathom, especially for users used to deal with REST, where is normal to expect a request to either succeed or fail.
Here is an example to make the concept more concrete:
(ns com.wsscode.pathom3.docs.demos.core.error-handling
(:require
[com.wsscode.pathom3.connect.operation :as pco]
[com.wsscode.pathom3.interface.eql :as p.eql]
[com.wsscode.pathom3.connect.indexes :as pci]
[com.wsscode.pathom3.error :as p.error]))
(def identity-db
{1 {:user/name "Martin"}})
(pco/defresolver user-identity
[{:keys [user/id]}]
{::pco/output [:user/name]}
(get identity-db id))
(def movies-db
{1 {:movie/title "Bacurau"}})
(pco/defresolver movie-details
[{:keys [movie/id]}]
{::pco/output [:movie/title]}
(get movies-db id))
(def env
(pci/register
{::p.error/lenient-mode? true}
[user-identity movie-details]))
(p.eql/process env {:movie/id 1}
[:user/name :movie/title])
; => {:movie/title "Bacurau",
; :com.wsscode.pathom3.connect.runner/attribute-errors {:user/name {:com.wsscode.pathom3.error/cause :com.wsscode.pathom3.error/attribute-unreachable}}}
Considering the request and response, did this request succeed or failed?
Hard to say at the "request level", but at the attribute level it's clear: :movie/title
succeed and :user/name
failed. At your application-level you must take this partial
failure property into account to decide what to do.
Note in lenient mode, Pathom adds the attribute ::pcr/attribute-errors
when there
is some attribute error. This map contains the error details for each attribute.
The error indicates the attribute :user/name
wasn't reachable, which makes sense given we
didn't provide the :user/id
required for it (and neither there is a resolver to
provide it).
Types of attribute errors
In the following diagram you can see how Pathom gets to each error type for a given attribute:
Attribute not requested
:com.wsscode.pathom3.error/attribute-not-requested
This error will tell that the attribute wasn't part of the request, or part of the initial data:
; using the same previous setup
(let [response (p.eql/process env {:movie/id 1}
; note this time we don't ask for the :user/name
[:movie/title])]
; but check for its error
(p.error/attribute-error response :user/name))
; => #:com.wsscode.pathom3.error{:cause :com.wsscode.pathom3.error/attribute-not-requested}
Attribute unreachable
:com.wsscode.pathom3.error/attribute-unreachable
This is the
Pathom considers the value nil
a success.
Node Errors
:com.wsscode.pathom3.error/node-errors
At this point, it means Pathom expected to fulfill the attribute, but something failed in the process.
The Pathom Plan includes an index that says which node is expected to return the value for each attribute (and if there are multiple options for an attribute, it will have multiple nodes associated with).
For each of the nodes, Pathom will look for errors (check again on the diagram to see the flow).
Node exception
:com.wsscode.pathom3.error/node-exception
When the resolver throws an exception, the runner stores this associated with the node that invoked the resolver. This is the first thing that Pathom looks for, and if there is an error there, it returns the error.
(let [response (p.eql/process
(pci/register
(pbir/constantly-fn-resolver :error-demo
(fn [_] (throw (ex-info "Example Error" {})))))
[:error-demo])]
(p.error/attribute-error response :error-demo))
; =>
{:com.wsscode.pathom3.error/cause
:com.wsscode.pathom3.error/node-errors,
:com.wsscode.pathom3.error/node-error-details
{1
{:com.wsscode.pathom3.error/cause
:com.wsscode.pathom3.error/node-exception,
:com.wsscode.pathom3.error/exception
{:cause "Example Error"
:data {}
:via [{:type clojure.lang.ExceptionInfo
:message "Example Error"
:data {}
:at [...]}]
:trace ...}}}}
Attribute missing on output
:com.wsscode.pathom3.error/attribute-missing
This happens when the resolver completed with success, but the output didn't include the attribute.
(let [response (p.eql/process
(pci/register
(pco/resolver 'x
{::pco/output [:a :b :c]}
(fn [_ _]
{:b 1 :c 2})))
[:a])]
(p.error/attribute-error response :a))
; =>
{:com.wsscode.pathom3.error/cause
:com.wsscode.pathom3.error/node-errors,
:com.wsscode.pathom3.error/node-error-details
{1
{:com.wsscode.pathom3.error/cause
:com.wsscode.pathom3.error/attribute-missing}}}
Ancestor error
:com.wsscode.pathom3.error/ancestor-error
In this case, the error didn't happen in the node responsible for the attribute, but on some ancestor node (some dependency behind on the chain).
{:com.wsscode.pathom3.error/cause
:com.wsscode.pathom3.error/node-errors,
:com.wsscode.pathom3.error/node-error-details
{1
{:com.wsscode.pathom3.error/cause
:com.wsscode.pathom3.error/ancestor-error,
:com.wsscode.pathom3.error/error-ancestor-id
2,
:com.wsscode.pathom3.error/exception
#error {
:cause "Example Error"
:data {}
:via [{:type clojure.lang.ExceptionInfo
:message "Example Error"
:data {}
:at [...]}]
:trace [...]}}}}
In this case Pathom indicates the ID of the ancestor node that failed, and the exception there. You can view the node relationship in the graph below:
Multiple errors
If there are many options for an attribute, you will get the error details of each path that failed.
(let [response (p.eql/process
(pci/register
[(pco/resolver 'err1
{::pco/output [:error-demo]}
(fn [_ _] (throw (ex-info "One Error" {}))))
(pco/resolver 'err2
{::pco/output [:error-demo]}
(fn [_ _] (throw (ex-info "Other Error" {}))))])
[:error-demo])]
(p.error/attribute-error response :error-demo))
; =>
{:com.wsscode.pathom3.error/cause
:com.wsscode.pathom3.error/node-errors,
:com.wsscode.pathom3.error/node-error-details
{1
{:com.wsscode.pathom3.error/cause
:com.wsscode.pathom3.error/node-exception,
:com.wsscode.pathom3.error/exception
#error {
:cause "Other Error"
:data {}
:via [{:type clojure.lang.ExceptionInfo
:message "Other Error"
:data {}
:at [...]}]
:trace
[...]}},
2
{:com.wsscode.pathom3.error/cause
:com.wsscode.pathom3.error/node-exception,
:com.wsscode.pathom3.error/exception
#error {
:cause "One Error"
:data {}
:via [{:type clojure.lang.ExceptionInfo
:message "One Error"
:data {}
:at [...]}]
:trace [...]}}}}
Observing resolver errors
You may want to know every time some error happen, you can use the
::pcr/wrap-resolver-error
extension:
(p.eql/process
(-> (pci/register
[(pco/resolver 'err1
{::pco/output [:error-demo]}
(fn [_ _] (throw (ex-info "One Error" {}))))
(pco/resolver 'err2
{::pco/output [:error-demo]}
(fn [_ _] (throw (ex-info "Other Error" {}))))])
(p.plugin/register
{::p.plugin/id 'err
:com.wsscode.pathom3.connect.runner/wrap-resolver-error
(fn [_]
(fn [env node error]
(println "Error: " (ex-message error))))}))
[:error-demo])
; Error: Other Error
; Error: One Error
; => {}
This extension gets invoked anytime a resolver error happens, normal or batch.
If you like to have more specific handlers for batches, there is also the ::pcr/wrap-batch-resolver-error.
Mutation Errors
Mutation errors are simple. In strict mode it will throw the exception, similar to how it handles resolvers.
On lenient mode, when a mutation fails, the value of the output is going
to be a map with the key ::pcr/mutation-error
, which has the exception.
(p.eql/process
(pci/register
{::p.error/lenient-mode? true}
(pco/mutation 'doit
{}
(fn [_ _]
(throw (ex-info "Mutation error" {})))))
['(doit)])
; =>
{doit {:com.wsscode.pathom3.connect.runner/mutation-error
#error {
:cause "Mutation error"
:data {}
:via [{:type clojure.lang.ExceptionInfo
:message "Mutation error"
:data {}
:at [...]}]
:trace [...]}}}
Observing mutation errors
Similar to resolvers, you can observe mutations using the ::pcr/wrap-mutation-error
extension:
(p.eql/process
(-> (pci/register
(pco/mutation 'doit
{}
(fn [_ _]
(throw (ex-info "Mutation error" {})))))
(p.plugin/register
{::p.plugin/id 'err
:com.wsscode.pathom3.connect.runner/wrap-mutation-error
(fn [_]
(fn [env ast error]
(println "Error on" (:key ast) "-" (ex-message error))))}))
['(doit)])
Error on doit - Mutation error
; =>
'{doit {:com.wsscode.pathom3.connect.runner/mutation-error
#error {:cause "Mutation error"
:data {}
:via [{:type clojure.lang.ExceptionInfo
:message "Mutation error"
:data {}
:at [...]}]
:trace [...]}}}