docs: write error handling guide

This commit is contained in:
Zach Daniel 2020-07-15 21:17:57 -04:00
parent b549337dc2
commit e303afc0d6
No known key found for this signature in database
GPG key ID: C377365383138D4B
2 changed files with 47 additions and 2 deletions

View file

@ -0,0 +1,44 @@
# Error Handling
Error handling in Ash is still a work in progress, but the pattern is mostly formed at this point. There is a difficult balance to cut between informative errors and enabling simple reactions to those errors. Since many extensions may need to work with and/or adapt their behavior based on errors coming from Ash, we need rich error messages. However, when you have a hundred different exceptions to represent the various kinds of errors a system can produce, it becomes difficult to say something like "try this code, and if it is invalid, do x, if it is forbidden, do y.
## Error Classes
To this effect, exceptions in Ash have one of four classes mapping to the top level exceptions:
- forbidden - `Ash.Error.Forbidden`
- invalid - `Ash.Error.Invalid`
- framework - `Ash.Error.Framework`
- unknown - `Ash.Error.Unknown`
Since many actions can be happening at once, we want to support the presence of multiple errors as a result of a request to ash. We do this by grouping up the errors into one before returning or raising.
We choose an exception based on the order of the exceptions listed above. If there is a single forbidden, we choose `Ash.Error.Forbidden`, if there is a single invalid, we choose `Ash.Error.Invalid` and so on. The actual errors will be included in the `errors` key on the exception. The exception's message will contain a bulleted list of all the underlying exceptions that occured. This makes it easy to react to specific kinds of errors, as well as to react to _any/all_ of the errors present.
An example of a single error being raised, representing multiple underlying errors:
```elixir
AshExample.Representative
|> Ash.Changeset.new(%{employee_id: "dabes"})
|> AshExample.Api.create!()
** (Ash.Error.Invalid) Input Invalid
* employee_id: must be absent.
* first_name, last_name: at least 1 must be present.
(ash 1.3.0) lib/ash/api/api.ex:534: Ash.Api.unwrap_or_raise!/1
```
This allows easy rescuing of the major error classes, as well as inspection of the underlying cases
```elixir
try do
AshExample.Representative
|> Ash.Changeset.new(%{employee_id: "dabes"})
|> AshExample.Api.create!()
rescue
e in Ash.Error.Invalid ->
"Encountered #{Enum.count(e.errors)} errors"
end
"Encountered 2 errors"
```
This approach is relatively experimental. I haven't seen it done this way elsewhere, but it seems like a decent middle ground from a system that can generate multiple disparate errors on each pass.

View file

@ -41,11 +41,12 @@ defmodule Ash.MixProject do
extra_section: "GUIDES",
extras: [
"documentation/introduction/getting_started.md",
"documentation/topics/validation.md"
"documentation/topics/validation.md",
"documentation/topics/error_handling.md"
],
groups_for_extras: [
Introduction: Path.wildcard("documentation/introduction/*.md"),
Topics: Path.wildcard("documentation/topics/*.md")
Topics: Path.wildcard("documentation/topics/*")
],
groups_for_modules: [
entrypoint: [