ash icon indicating copy to clipboard operation
ash copied to clipboard

Add qualitative context when building ash errors to be raised/returned

Open zachdaniel opened this issue 3 years ago • 1 comments

Because of the declarative nature of Ash, the stacktrace of an error is often not very informative. When we create an "error class" to return, we need to annotate it with whatever additional information we have to aid in debugging. For example, the resource + action combo.

zachdaniel avatar Jul 05 '22 13:07 zachdaniel

This would essentially just amount to going to every place where we do Ash.Error.to_error_class and passing in an extra string, and then accumulating those strings (in the case that we pass an error class into to_error_class) and showing them in the error class message.

zachdaniel avatar Jul 05 '22 13:07 zachdaniel

So the ultimate idea is that "more context on errors is better". The difficult thing in Ash is that the stacktrace is often practically useless because it is all generic, i.e:

       (ash 2.2.0) lib/ash/error/changes/invalid_attribute.ex:5: Ash.Error.Changes.InvalidAttribute.exception/1
       (ash 2.2.0) lib/ash/changeset/changeset.ex:3167: anonymous fn/3 in Ash.Changeset.add_invalid_errors/4
       (elixir 1.14.0) lib/enum.ex:2468: Enum."-reduce/3-lists^foldl/2-0-"/3
       (stdlib 4.1.1) maps.erl:411: :maps.fold_1/3
       (ash 2.2.0) lib/ash/changeset/changeset.ex:640: Ash.Changeset.do_for_action/4

We don't know for which resource, which attribute, or anything. And when you are doing managed relationships, or nested queries, you can often end up with errors that give you little to no help finding the root cause.

A solution (or perhaps just a salve) to this is to when we convert a given error to an error class using Ash.Error.to_error_class or when we create an error with Ash.Error.to_ash_error(...) we could support an optional value like context, that gets stored on each error. Then, when printing out the error, we could show the context like breadcrumbs above each message.

error = Ash.Error.to_ash_error("whoops!", nil, context: "calling data layer create")

Ash.Error.to_error_class(error, context: "running create action")

And that would produce an error message like:

Context: running create action
** (Ash.Error.Invalid) Input Invalid
     
     Context: running create action > calling data layer create
     * whoops!

Then, we can sprinkle these contexts in various useful places. This will necessitate adding a context field to all ash errors (this is done in def_ash_error so will be quite easy), which should be a list of strings. We will also need to add that context field to the error classes. Then we adjust the message/1 function for the error classes to display these breadcrumbs.

zachdaniel avatar Oct 25 '22 06:10 zachdaniel

Thanks for the extra details, @zachdaniel

rellen avatar Oct 27 '22 01:10 rellen

Great work! This has been merged. We can incrementally add more context over time, as we spot errors that can be improved.

zachdaniel avatar Nov 24 '22 01:11 zachdaniel