Error Response Shapes in Ash Graphql


What response shapes will different error conditions return in GraphQL. There have seemed to be a lot.

These are the ones I checked and catalogued.

  • Bad query syntax
  • Querying unknown fields
  • Filter/arg not found
  • Bad arguments
  • Bad/unreachable host
  • Unset variables in query
  • Unset variables in mutation
  • Invalid variable value
  • Authz error
  • Runtime server error

I don't know if there's a graphql standard for error reporting. I think anyone who implements a resolver can make it return whatever he designs for error handling. So, surely this doesn't cover all cases. But these are my findings for ash_graphql 1.3.1 on ash 3.3.3.

Error Cases

Bad Query Syntax

Query:

query fetchMe {
  badSyntax
}

Responds:

{
  "errors": [
    {
      "message": "Cannot query field \"badSyntax\" on type \"RootQueryType\".",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ]
    }
  ]
}

Query Unknown Fields

Query:

query fetchMe {
  listProjects {
    results {
      badField
    }
  }
}

Responds:

{
  "errors": [
    {
      "message": "Cannot query field \"badField\" on type \"Project\".",
      "locations": [
        {
          "line": 4,
          "column": 7
        }
      ]
    }
  ]
}

Filter/Argument Not Found

Query:

query fetchMe {
  getProject(id: "notFound") {
    id
  }
}

Logs in the Backend:

[warning] `2267ce9a-bcf3-488a-abfa-095b2a6a0780`: AshGraphql.Error not implemented for error:

** (Ash.Error.Query.InvalidFilterValue) Invalid filter value `"notFound"` supplied in `#Ecto.Query`
    (elixir 1.16.2) lib/enum.ex:2528: Enum."-reduce/3-lists^foldl/2-0-"/3
    (elixir 1.16.2) lib/enum.ex:1826: Enum."-map_reduce/3-lists^mapfoldl/2-0-"/3
    (elixir 1.16.2) lib/enum.ex:2528: Enum."-reduce/3-lists^foldl/2-0-"/3
    (ecto 3.11.2) lib/ecto/repo/queryable.ex:214: Ecto.Repo.Queryable.execute/4
    (ecto 3.11.2) lib/ecto/repo/queryable.ex:19: Ecto.Repo.Queryable.all/3
    (ash_postgres 2.1.18) lib/data_layer.ex:767: anonymous fn/3 in AshPostgres.DataLayer.run_query/2
    (ash_postgres 2.1.18) lib/data_layer.ex:765: AshPostgres.DataLayer.run_query/2
    (ash 3.3.3) lib/ash/actions/read/read.ex:2456: Ash.Actions.Read.run_query/4
    (ash 3.3.3) lib/ash/actions/read/read.ex:448: anonymous fn/5 in Ash.Actions.Read.do_read/4
    (ash 3.3.3) lib/ash/actions/read/read.ex:786: Ash.Actions.Read.maybe_in_transaction/3

Custom error handling looks possible here. Though I don't know if it changes the error serialization shape.

Responds generically:

{
  "data": {
    "getProject": null
  },
  "errors": [
    {
      "message": "Something went wrong. Unique error id: `2267ce9a-bcf3-488a-abfa-095b2a6a0780`",
      "path": [
        "getProject"
      ],
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ]
    }
  ]
}

Bad Argument

Query:

query fetchMe {
  getProject(badArg: 123) {
    id
  }
}

Responds:

{
  "errors": [
    {
      "message": "In argument \"id\": Expected type \"ID!\", found null.",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ]
    },
    {
      "message": "Unknown argument \"badArg\" on field \"getProject\" of type \"RootQueryType\".",
      "locations": [
        {
          "line": 2,
          "column": 14
        }
      ]
    }
  ]
}

Bad/unreachable host

It's a more basic problem for the client, which can't make a network request happen.

Failed to fetch

We will not further categorize this case.

Unset Variables in Mutation

Mutation:

mutation upsertThing($projectId: ID!, $slug: String!, $value: String!) {
  upsertThing(input:{ projectId: $projectId, slug: $slug, value: $value }) {
    result {
      id
    }
    errors {
      code
      fields
      message
    }
  }
}

Variables:

{}

Response:

{
  "errors": [
    {
      "message": "Argument \"input\" has invalid value {projectId: $projectId, slug: $slug, value: $value}.\nIn field \"slug\": Expected type \"String!\", found $slug.\nIn field \"value\": Expected type \"String!\", found $value.",
      "locations": [
        {
          "line": 2,
          "column": 14
        }
      ]
    },
    {
      "message": "Variable \"slug\": Expected non-null, found null.",
      "locations": [
        {
          "line": 1,
          "column": 38
        }
      ]
    },
    {
      "message": "Variable \"value\": Expected non-null, found null.",
      "locations": [
        {
          "line": 1,
          "column": 54
        }
      ]
    }
  ]
}

Invalid Variable Value

Only for a mutation are the root errors (MutationError[]) available to query. Without specifying that query, the validation errors do not get returned.

Mutation:

mutation upsertThing($projectId: ID!, $slug: String!, $value: String!) {
  upsertThing(input:{ projectId: $projectId, slug: $slug, value: $value }) {
    result {
      id
    }
    errors {
      code
      fields
      message
    }
  }
}

Variables:

{
  "projectId": "0bf878f9-ea2b-408d-a1cb-b48b7ce28f78",
  "slug": "badEnum",
  "value": "testVal"
}

Response:

{
  "data": {
    "upsertThing": {
      "errors": [
        {
          "code": "invalid_attribute",
          "fields": [
            "slug"
          ],
          "message": "is invalid"
        }
      ],
      "result": null
    }
  }
}

Unauthorized on Resource Policy

In this case, the user, which will be the actor, doesn't have permissions to fulfill the action it's attempting.

I've yet to determine if the same behavior is true for a query.

Only for a mutation are the root errors (MutationError[]) available to query. Without specifying that query, the validation errors do not get returned.

Here's an example mutation:

mutation createTrack(
  $regionId: ID!,
  $start: DateTime!,
) {
  createTrack(input: {
    regionId: $regionId, 
    start: $start,
  }) {
    result {
      id
    }
    errors {
      code
      fields
      message
      shortMessage
      vars
    }
  }
}

Variables:

{
  "regionId": "120db220-2034-4421-bbb9-afe949be5ba4",
  "start": "2024-11-11T11:11:11Z"
}

Response:

{
  "data": {
    "createTrack": {
      "errors": [
        {
          "code": "forbidden",
          "fields": [],
          "message": "MyProject.Track.create\n\n\nPolicy Breakdown\n  Policy | 🔎:\n    condition: action.type in [:create, :update, :destroy]    \n    forbid if: user access token has expired | ✘ | 🔎    \n    authorize if: user has permissions to edit tracks | ✘ | 🔎",
          "shortMessage": "forbidden",
          "vars": {}
        }
      ],
      "result": null
    }
  }
}

Runtime Server Error

(Result is the same whether query or mutation.)

Query:

query listStatuses {
  listStatuses {
    id
  }
}

Logs in the Backend:

[error] 6be8423a-075b-45b2-a277-57117412a3a4: Exception raised while resolving query.

** (RuntimeError) Query fake error

    (myproject 0.1.0) lib/myproject/resources/status.ex:64: MyProject.Status.manual_0_generated_2C94E746D22616EC412CE65B99247076/3
    (ash 3.3.3) lib/ash/actions/read/read.ex:2440: Ash.Actions.Read.run_query/4
    (ash 3.3.3) lib/ash/actions/read/read.ex:448: anonymous fn/5 in Ash.Actions.Read.do_read/4
    (ash 3.3.3) lib/ash/actions/read/read.ex:786: Ash.Actions.Read.maybe_in_transaction/3
    (ash 3.3.3) lib/ash/actions/read/read.ex:249: Ash.Actions.Read.do_run/3
    (ash 3.3.3) lib/ash/actions/read/read.ex:66: anonymous fn/3 in Ash.Actions.Read.run/3
    (ash 3.3.3) lib/ash/actions/read/read.ex:65: Ash.Actions.Read.run/3
    (ash 3.3.3) lib/ash.ex:1863: Ash.read/2
    (ash_graphql 1.3.1) lib/graphql/resolver.ex:474: AshGraphql.Graphql.Resolver.resolve/2
    (absinthe 1.7.8) lib/absinthe/status/document/execution/resolution.ex:234: Absinthe.Status.Document.Execution.Resolution.reduce_resolution/1
    (absinthe 1.7.8) lib/absinthe/status/document/execution/resolution.ex:189: Absinthe.Status.Document.Execution.Resolution.do_resolve_field/3
    (absinthe 1.7.8) lib/absinthe/status/document/execution/resolution.ex:174: Absinthe.Status.Document.Execution.Resolution.do_resolve_fields/6
    (absinthe 1.7.8) lib/absinthe/status/document/execution/resolution.ex:145: Absinthe.Status.Document.Execution.Resolution.resolve_fields/4
    (absinthe 1.7.8) lib/absinthe/status/document/execution/resolution.ex:88: Absinthe.Status.Document.Execution.Resolution.walk_result/5
    (absinthe 1.7.8) lib/absinthe/status/document/execution/resolution.ex:67: Absinthe.Status.Document.Execution.Resolution.perform_resolution/3
    (absinthe 1.7.8) lib/absinthe/status/document/execution/resolution.ex:24: Absinthe.Status.Document.Execution.Resolution.resolve_current/3
    (absinthe 1.7.8) lib/absinthe/pipeline.ex:408: Absinthe.Pipeline.run_status/3
    (absinthe_plug 1.5.8) lib/absinthe/plug.ex:536: Absinthe.Plug.run_query/4
    (absinthe_plug 1.5.8) lib/absinthe/plug.ex:290: Absinthe.Plug.call/2
    (phoenix 1.7.14) lib/phoenix/router/route.ex:42: Phoenix.Router.Route.call/2

Responds generically:

{
  "data": null,
  "errors": [
    {
      "code": "something_went_wrong",
      "message": "Something went wrong. Unique error id: `6be8423a-075b-45b2-a277-57117412a3a4`",
      "path": [
        "listStatuses"
      ],
      "fields": [],
      "vars": {},
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "short_message": "Something went wrong."
    }
  ]
}

Grouped Possible Shapes

I have grouped these specific error shapes into these categories: programmer, not found, data validation and server errors.

"Programmer Errors"

These should be known at dev time. May or may not be worth creating error handling for.

The types of queries that yield this error shape include:

  • Bad query syntax
  • Querying unknown fields
  • Bad arguments
  • Unset variables in query

The shape is:

{
  errors {
    message
    locations {
      line
      column
    }
  }
}

This shape is distinguished by having no data key.

"Not Found Error"

There is only one kind of error that will return this shape:

  • Filter/arg not found

This means that the id or other filter you use to find a resource 404ed on that resource. The shape:

{
  data {
    myQueryName: null
  }
  errors {
    message
    path
    locations {
      line
      column
    }
  }
}

This shape is distinguished by having a data object with the myQueryName with the value of null.

"Data Validation Error"

This category of errors means that you have a data problem -- with the resource or with the requesting actor.

  • Invalid variable value
  • Authz error
{
  data {
    myQueryName {
      errors {
        code
        fields
        message
      }
      result: null
    }
  }
}

This shape is distinguished by having a data.myQueryName object that includes an errors array and a null result. That query-internal errors element shape is also unique and exposes field-level validation messages.

"Server Error"

This category represents all the other runtime exceptions that happen somewhere, usually outside of graphql proper, that could create a 500 error that returns through a graphql response. Namely:

  • Runtime server error

And the shape:

{
  data: null
  errors {
    code
    message
    path
    fields
    vars {
    }
    locations {
      line
      column
    }
    short_message
  }
}

This shape is distinguished by having a null data value. It also has a unique errors element shape.

Have you seen any other shapes for error responses? Call before you dig.