Handling nullability and errors
Make your queries even more typesafe
Nullability annotations are currently experimental in Apollo Kotlin. If you have feedback on them, please let us know via GitHub issues, in the Kotlin Slack community, or in the GraphQL nullability working group.
Nullability in GraphQL
GraphQL does not have a Result
type. If a field errors, it is set to null
in the JSON response and an error is added to the errors
array.
With the following schema and query:
type User {id: ID!# name is nullable, but in practice this happens only in error casesname: StringavatarUrl: String}query GetUser {user {idnameavatarUrl}}
The server returns the following response in case of error:
{"data": {"user": {"id": "1001","name": null,"avatarUrl": "https://example.com/pic.png"}},"errors": [{"message": "Cannot resolve user.name","path": ["user", "name"]}]}
GraphQL best practices recommend using nullable types by default. From graphql.org:
In a GraphQL type system, every field is nullable by default. This is because there are many things that can go awry in a networked service backed by databases and other services.
This default makes sense in distributed environments where microservices might fail and some data fail to resolve. This behaviour is robust by default: if part of your query fails, you still get the rest of the data.
This default has one major drawback for frontend developers though. It requires to carefully check every field in your UI code.
Sometimes it's not clear how to handle the different cases:
@Composablefun User(user: GetUserQuery.User) {if (user.name != null) {Text(text = user.name)} else {// What to do here?// Is it an error?// Is it a true null?// Should I display a placeholder? an error? hide the view?}}
To help deal with those issues and make sure your UI code only sees what make sense for your app, Apollo Kotlin offers three tools:
These tools change effectively the GraphQL default from "handle every field error" to "opt-in the errors you want to handle".
Nullability in Apollo Kotlin
Error aware parsing
With error aware parsing, the errors are detected at parsing time, so you don't have to look them up in the errors
array. Any GraphQL error stops the parsing and you are guaranteed that response.data
is actual semantic data and does not contain any error.
In order to opt-in error aware parsing, create an extra.graphqls
file next to your schema and import the @catch
directive:
# Import the `@catch` directive definition.# When it is imported, parsers generate extra code to deal with errors.extend schema @link(url: "https://specs.apollo.dev/nullability/v0.3",import: ["@catch", "CatchTo"])
And choose your default error handling. You can choose to coerce errors to null
, like the current GraphQL default:
# Coerce errors to null by default. Partial data is returned. This partial data may contain errors that need to be checked# against the `errors` arrayextend schema @catch(to: NULL)
Or re-throw errors:
# Errors stop the parsing. The data never contains error. Use `@catch` in operations to let partial data throughextend schema @catch(to: THROW)
In the @catch(to: THROW)
mode, the parsing stops at the first GraphQL error and the whole query is failed. In the example above:
{"data": {"user": {"id": "1001","name": null,"avatarUrl": "https://example.com/pic.png"}},"errors": [{"message": "Cannot resolve user.name","path": ["user", "name"]}]}
response.data
is null and response.exception
is an instance of ApolloGraphQLException
:
println(response.data) // nullprintln(response.exception) // ApolloGraphQLException
This default makes it easy to handle errors as they are all handled at parsing time.
Any null
in the response is guaranteed to be a semantic null and not an error, and you should probably handle it.
Note: if you have existing code that uses partial data, you can use @ignoreErrors
to disable throwing on a query
per query basis.
Sometimes failing the whole query feels a bit overkill though. For those cases, you can handle errors with @catch
.
Handle errors and receive partial data with @catch
In some cases, you might want to display the data returned from the server, even if it is partial.
To do so, catch GraphQL errors using the @catch
directive:
query GetUser {user {id# map name to FieldResult<String?> instead of stopping parsingname @catchavatarUrl}}
Instead of failing the whole query, the name
field is now generated as a FieldResult<String?>
class that can contain either a nullable value or an error:
// name does not have a valueprintln(response.data?.user?.name?.valueOrNull) // null// but name has an errorprintln(response.data?.user?.name?.graphQLErrorOrNull?.message) // "Cannot resolve user.name"// partial data is availableprintln(response.data?.user?.id) // "42"// exception is nullprintln(response.exception) // null
For fields of List type, @catch
applies only to the first level. If you need to apply it to a given level, use the levels
argument:
query GetUser {user {# socialLinks fails the operation on error# socialLinks[0], socialLinks[1], socialLinks[2], etc... are mapped to FieldResult classessocialLinks @catch(levels: [1])}}
For fields of composite type, @catch
catches any nested error:
query GetUser {# catch errors on user and any nested fielduser @catch(to: RESULT) {nameavatarUrl}}
Response:
{"data": {"user": {"id": "1001","name": null,"avatarUrl": "https://example.com/pic.png"}},"errors": [{"message": "Cannot resolve user.name","path": ["user", "name"]}]}
data.user
is a FieldResult<User>
:
// user is a FieldResult<User> that contains the name errorprintln(response.data?.user?.graphQLErrorOrNull?.message) // "Cannot resolve user.name"
Handle semantic non-null with @semanticNonNullField
Even with automatic detection of errors and handling of partial data like described above, you still have to check for null
in your UI code. This is because of the same reason that GraphQL by default recommends nullable fields. Your backend team has no other choice than make a field nullable if it can fail.
But a field like 'User.name' is semantically non-null: it should never be null
, except when something goes really wrong.
In those cases, you can mark a field as "semantic non-null" and the Apollo Kotlin compiler will generate it as non-nullable even though it is nullable in the server schema. To do so, add @semanticNonNullField
to your extra.graphqls
file:
# extra.graphqls# Import the `@semanticNonNull` directive definition in addition to `@catch` and `CatchTo`.extend schema @link(url: "https://specs.apollo.dev/nullability/v0.3",import: ["@catch", "CatchTo", "@semanticNonNullField"])# mark User.name as semantic non-nullextend type User @semanticNonNullField(name: "name")
name
is now a non-null Kotlin types:
class User(val id: String,// name is non-null hereval name: String,val avatarUrl: String?,)
If, by any chance, an error happens that you still want to handle on that field, you can use @catch
to map name
to a FieldResult<String>
property containing a non-null value or an error.
user.name.valueOrNull // "Luke Skywalker"user.name.graphQLErrorOrNull // null
Similarly to @catch
, @semanticNonNullField
supports a levels
argument for lists:
# User.friends is generated a nullable# User.friends[0], User.friends[1], User.friends[2], etc.. is generated a non-nullextend type User @semanticNonNullField(name: "friends", levels: [1])
Migrate progressively with @ignoreErrors
@ignoreErrors
is an operation directive that disables error aware parsing for a specific operation:
"""Never throw on errors.This is used for backward compatibility for clients where this was the default behaviour."""directive @ignoreErrors on QUERY | MUTATION | SUBSCRIPTION
To enable it, import it like @catch
, CatchTo
and @semanticNonNullField
:
extend schema @link(url: "https://specs.apollo.dev/nullability/v0.3",import: ["@catch", "CatchTo", "@semanticNonNullField", "@ignoreErrors"])
Enabling error aware parsing and adding @ignoreErrors
to all your operations is a no-op. You can then remove @ignoreErrors
from your operations progressively.
Decision flowchart
Different teams have different needs:
- Some teams have a schema where a lot of fields are non-null already and might know from their backend team that all the nullable ones are semantic nulls. For those teams, there is little value in using
@catch
or@semanticNonNullField
. - Other teams have a schema where fields are all nullable by default, according to the GraphQL best practices.
- Some of them want to display as much data as possible always. The frontend team needs to check for errors on every field and this makes the experience very robust at the price of more complex client code.
- Some of them want to opt-in the errors they want to handle and save the code to check every field.
Finally, some teams want to opt-in error aware parsing without breaking their existing codebase. For those cases, @ignoreErrors
allows a more progressive solution.