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*
note
  • 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 Pathom 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.

info

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:

Loading viz data...

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

important

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:

Loading viz data...

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 [...]}}}}
Loading viz data...

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. 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
(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 [...]}}}