Unable to return errors when List [T] contains null values

I’m trying to do the same thing described in this issue: Allow returning data and errors from a resolver · Issue #512 · absinthe-graphql/absinthe · GitHub

That is, I want to create a mutation field whose return type looks like this:

type MutationPayload {
  entities: [Entity]
}

And handle partial success with a response like this:

{
  data: {
    createEntities: {
     entities: [ {text: "hi"}, null, {text: "hello"} ]
   }
  }, 
  errors: [{ message: "some error related to entity with id 123" }]
}

In the linked issue above, @benwilson512 says that this response is not allowed by the spec. I’ve been discussing this subject with my team, and there is some doubt about this interpretation. I think it’s informed by the Response section of the spec, specifically this paragraph:If an error was encountered during the execution that prevented a valid response, the data entry in the response should be null.

However, the List section seems to suggest that the response described above is in fact valid: If a list’s item type is nullable, then errors occuring during preparation or coercion of an individual item in the list must result in a the value null at that position in the list along with an error added to the response.

The Absinthe library disallows this response, as described in the github issue. I can confirm that this is the case. Do folks have any thoughts about how to approach this? It seems to me that the library isn’t allowing something that should be allowed by the spec, and I’m hoping to find a workaround.

Hi @michaelcaterisano welcome!

I could be mistaken, but I think you’re conflating two distinct things. The section you have linked to is entitled “Result coercion” and it has to do with coercing a result from the internal programming language value into a GraphQL type. It isn’t speaking directly to the question of whether a resolver on the createEntities field can return both data and errors.

This section here describes how resolvers ought to work, and if you scroll down to here you get the bit I rely on for interpretation:

If an error is thrown while resolving a field, it should be treated as though the field returned null, and an error must be added to the “errors” list in the response.

That doesn’t seem to leave a lot of room to do what you want as a return value from a resolver. Now, what you can do is basically:

type MutationPyaload {
  entities: [EntityResult]
}

type EntityResult {
  entity: Entity!
}

Then you can return this sort of thing in your mutation resolver:

result = [%{entity: entity1}, %{error: {:error, "couldn't make entity 123"}}, %{entity: entity3}]

{:ok, result}

Then your entity_result object would look like this:

object :entity_result do
  field :entity, :entity, resolve fn
    %{entity: entity}, _, _ -> {:ok, entity}
    %{error: error}, _, _ -> error
  end
end

This will produce results that look basically like this:

{
  data: {
    createEntities: {
     entities: [ {entity: {text: "hi"}}, null, {entity: {text: "hello"}} ]
   }
  }, 
  errors: [{ message: "some error related to entity with id 123"}]
}

Again happy to be wrong, but the actual resolution part of the spec seems pretty clear to me.

@benwilson512 While your suggestion would work and is a workaround we’re considering, we’d really like to return a single payload type with some top-level fields, like this:

type MutationPayload {
  entities: [Entity]
  metadata: SomeMetadataType
}

To your point about this sentence:

If an error is thrown while resolving a field, it should be treated as though the field returned null, and an error must be added to the “errors” list in the response.

The interpretation that an error raised while resolving a list of nullable types should cause the entire field to return null seems to make it impossible to return a list of nullable types whose values are a mixture of data and null, adding errors to the error key, as described here:

If a list’s item type is nullable, then errors occuring during preparation or coercion of an individual item in the list must result in a the value null at that position in the list along with an error added to the response.

I think the question is whether list items are considered fields. The quote above seems to suggest that they are, no?

Just to be clear, my solution returns errors as errors to the client, not as data. It is returning errors as part of the Elixir resolver return value sure, but then it’s converted to a GraphQL error here:

  field :entity, :entity, resolve fn
    %{entity: entity}, _, _ -> {:ok, entity}
    %{error: error}, _, _ -> error
  end

Notice how in the mutation resolver result item at index 1 is %{error: {:error, "reason"}} and so the value is unpacked from the %{error: key it means that the resolver is returning {:error, "reason"} which means you get a proper GraphQL error.

That is the question, and they definitely are not, nor is “coercion” or “preparation” the same as resolution or execution. If you read through the “Executing fields” section GraphQL I think it’s pretty clear that a given item in a list cannot also be a field. It doesn’t take arguments, it doesn’t have a distinct selection set, etc.

The other thing to emphasize is that we’re talking about the resolution of a specific mutation field. That field has a single return value. That value can be a list type, but that list type is not then N resolutions, where there is a distinct resolution for each item in the list. Rather there is one resolution (the resolution of the mutation field) that returns one value (a list), there is a “result coercion” of that value, which involves a result coercion of each item in the list. Then fields in the selection set on the result are executed on each result. There is no field that represents individual item from parent list -> individual item | null | errors, nor is there a resolver for such.

1 Like

Yeah sorry about that, I caught that after I wrote it and edited my post above.

Given the interpretation of the spec you’re describing, I’m not clear on how the response described in the List section of the spec would ever be possible. What am I missing? The following suggests that a response like [item, null, item] and an error on the errors key related to the null result should be possible:

If a list’s item type is nullable, then errors occurring during preparation or coercion of an individual item in the list must result in a the value null at that position in the list along with a field error added to the response.

I tried to find a clear definition of “preparation” in the spec, but I only found “preparation” and “preparing” used in two places, and both are a bit vague. I guess we’re saying that it relates specifically to type coercion? IMO it’s not fully clear what the spec means by “preparation”. It seems to me that explicitly returning null for an element in a list should be possible.

And regarding this:

but that list type is not then N resolutions, where there is a distinct resolution for each item in the list

Isn’t it true that for each entity in a list [Entity], the query resolver for entity will be called?

Apologies I’m on my phone now so I can’t construct an example of the coercion scenario I’m talking about. I believe the idea is that if you return like [%{foo: 1}, 1, %{foo: 2}] the middle integer cannot coerce to an object, so you get an error.

Which resolver? Can you construct a minimal example that shows the resolution behavior you expect?

Apologies I’m on my phone now so I can’t construct an example of the coercion scenario I’m talking about. I believe the idea is that if you return like [%{foo: 1}, 1, %{foo: 2}] the middle integer cannot coerce to an object, so you get an error.

This seems right to me.

A naive implementation of my scenario would look like this:

object :entity do
  field(:text, do: resolve(fn parent, _, _ -> {:ok, String.upcase(parent.text)} end)
end

object :create_entities_payload do
  field(:entities, list_of(:entity))
end

object :mutation do
  field :create_entities, :create_entities_payload do
    resolve(fn _, _, _ -> {:ok, %{entities: [%{text: "hi"}, nil]}})
  end
end

The mutation operation would look like this:

mutation SomeMutationOperation {
  createEntities {
    entities {
      text
    }
  }
}

The response in this case would be:

%{"data" => %{"createEntities" => [%{"entities" => [%{"text" => "HI"}, nil]}]}}

In other words, the field resolvers for entity are called for each element in the entities list, which in this case uppercases the value of text. Is this not resolution?

UPDATE:

@benwilson512 I think I see the problem I’m having. In order to create the response I want, I’d need to have data at createEntities.entities and also errors whose paths are createEntities.entities. In my example above, the error path would be createEntitites.entities.text if the text resolver returned an error.

It does seem unfortunate that the spec has this limitation in the case of a field whose value is a list of nullable types, though. The inability to return a list that describes partial success and also return error describing the failures leads to workarounds like returning errors as data, which IMO violates the spec.

We’ll probably end up doing what you suggested; that is, returning a list of payload types instead of a single payload type whose fields are lists. Thanks for the discussion.

Yes, this is exactly the issue. If you have:

mutation SomeMutationOperation {
  createEntities {
    entities {
      text
    }
  }
}

There are only three fields here.

  1. createEntities takes the root object, and returns a single create_entities_payload object
  2. entities takes a single create_entities_payload and returns a list of entities.
  3. text takes an individual entity, and returns a single string.

If you return an error on (3) it’s really just on the text field, but this is pretty close to what I was having you do with the wrapper object.

If you return an error on (2) you void the whole list, and obviously the same issue applies to (1).

I don’t disagree, it’s definitely annoying. I wonder if some of this has to do with the whole mental model around a “graph”. You either traverse the node to the type or you don’t. Not sure! Either way, let me know if you run into any snags in your wrapper effort, happy to help.

Likewise!