Skip to content

[RFC] Error handling -- @catch & partial data #5337

@martinbonnin

Description

@martinbonnin

Description

For previous episodes on error handling, visit #4711

Goal

Simplify the error handling which is currently based on 3 different properties of ApolloResponse:

  1. response.data for GraphQL data
  2. response.errors for GraphQL errors
  3. response.exception for network exceptions

This is confusing and ideally, we want ApolloResponse to be a result-like class with either data or an exception. There is a lot of brain muscle around result-like classes. Kotlin stdlib has Result, Arrow has Either, Rust has Result, etc... So it feels like a natural fit here.

Proposal: @catch client directive

Inspired by Relay error handling, introduce a @catch client directive that allows user to opt-in error handling on a given field.

If this field or any sub-field contains an entry in the errors array then this error is exposed to the user and no data is shown for that field.

Advantages: data contains everything needed to display the UI. There's no need to go back to the errors array because the error is now inline at the position where it happens. We could keep the errors arrays for advanced use cases but in the very large majority of use cases, it shouldn't be needed. We can rely on data or exception again.

A @catch field is modeled in generated models as a sealed interface and matching result types:

Schema:

type Query {
  product: Product!
}

type Product {
  name: String!
  price: Float!
}

Query:

{
  product @catch {
    name
    price
  }
}

Kotlin codegen:

class Data(val product: ProductResult)

sealed interface ProductResult
class ProductSuccess(val product: Product): ProductResult
class ProductError(val error: Error): ProductResult

class Product(val name: String, val price: Double)

Examples

Product error

Query:

{
  product @catch {
    name
    price
  }
}

Error Response:

{
  "errors": [
    {
      "message": "Cannot resolve product",
      "path": ["product"]
    }
  ],
  "data": {
    "product": null
  }
}

Kotlin test:

val response = apolloClient.query(Query()).execute()
val product = response.data.product
check(product is ProductError)
check(product.error.path == listOf("product"))

price error

If a nested fails, the error is exposed on the closest enclosing @catch field:

{
  "errors": [
    {
      "message": "Cannot resolve product price",
      "path": ["product", "price"]
    }
  ],
  "data": {
    "product": {
      "name": "Champagne",
      "price": null
    }
  }
}

Kotlin test:

val response = apolloClient.query(Query()).execute()
val product = response.data.product
check(product is ProductError)
check(product.erro.path == listOf("product"))

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions