Overview
Handling error states is a crucial part of any quality user experience, and one of GraphQL's strengths is its ability to express unexpected behaviors with precision. Apollo Kotlin gives developers a robust toolset for defining the control flow for anything that might go wrong in your application.
In this lesson, we will:
- Define different types of errors encountered when fetching from a GraphQL server
- Build an abstraction that defines success, error, and loading states
- Test different error flows within our app
We'll spend most of this lesson within the LaunchDetails.kt
file.
Handle errors
As you execute a query, different types of errors can happen:
- Fetch errors: connectivity issues, HTTP errors, JSON parsing errors, etc. In this case,
response.exception
contains anApolloException
. Bothresponse.data
andresponse.errors
are null. - GraphQL request errors: in this case,
response.errors
contains the GraphQL errors.response.data
is null. - GraphQL field errors: in this case,
response.errors
contains the GraphQL errors.response.data
contains partial data.
Let's handle the first two for now: fetch errors and GraphQL request errors.
First let's create a LaunchDetailsState
sealed interface at the top of the module, just below the import
s. This will encapsulate all possible states of the UI:
private sealed interface LaunchDetailsState {object Loading : LaunchDetailsStatedata class Error(val message: String) : LaunchDetailsStatedata class Success(val data: LaunchDetailsQuery.Data) : LaunchDetailsState}
Then, add the sealed interface to your imports:
import com.example.rocketreserver.LaunchDetailsState.Loadingimport com.example.rocketreserver.LaunchDetailsState.Successimport com.example.rocketreserver.LaunchDetailsState.Error
Then, in LaunchDetails
, examine the response returned by execute
and map it to a State
:
@Composablefun LaunchDetails(launchId: String, navigateToLogin: () -> Unit) {var state by remember { mutableStateOf<LaunchDetailsState>(Loading) }LaunchedEffect(Unit) {val response = apolloClient.query(LaunchDetailsQuery(launchId)).execute()state = when {response.data != null -> {// Handle (potentially partial) dataLaunchDetailsState.Success(response.data!!)}else -> {LaunchDetailsState.Error("Oh no... An error happened.")}}}// Use the state
Just below that, use the state to show the appropriate UI:
// Use the statewhen (val s = state) {Loading -> Loading()is Error -> ErrorMessage(s.message)is Success -> LaunchDetails(s.data, navigateToLogin)}}
And finally, we'll need to update our private
LaunchDetails
composable to accept the data
instead of response
:
@Composableprivate fun LaunchDetails(data: LaunchDetailsQuery.Data) {// everywhere within this function, replace `response?.data?.`// with `data.`. Example:// Mission patchAsyncImage(modifier = Modifier.size(160.dp, 160.dp),model = data.launch?.mission?.missionPatch,placeholder = painterResource(R.drawable.ic_placeholder),error = painterResource(R.drawable.ic_placeholder),contentDescription = "Mission patch")}
Enable airplane mode before clicking the details of a launch. You should see this:
This is good! We have successfully handled fetch and GraphQL request errors. Now we will tackle GraphQL field errors, which is often called "partial data."
Handle partial data
One of GraphQL's strengths is that clients can ask for only the data they need. But what happens when a server, for whatever reason, is only able to serve some of the data specified in the query, but not all of it? Well, in that case, both data
and errors
will be included in the response payload and we, as client developers, get to choose what to do with that in our app.
Handling this partial data in your UI code can be complicated. Let's modify our app code within the LaunchDetails
function to show how this can be done:
state = when {response.errors.orEmpty().isNotEmpty() -> {// GraphQL errorError(response.errors!!.first().message)}response.exception is ApolloNetworkException -> {// Network errorError("Please check your network connectivity.")}response.data != null -> {// data (never partial)Success(response.data!!)}else -> {// Another fetch error, maybe a cache miss?// Or potentially a non-compliant server returning data: null without an errorError("Oh no... An error happened.")}}
For the purpose of this lesson, we're going to treat field errors the same as request errors and won't attempt to render a UI with partial data. But GraphQL and Apollo Kotlin allow for significant flexibility on how to handle partial data and unexpected null values.
Let's test our new control flow. To trigger a GraphQL field error, replace LaunchDetailsQuery(launchId)
with LaunchDetailsQuery("invalidId")
. Disable airplane mode and select a launch. The server will send this response:
{"errors": [{"message": "Launch not found","locations": [{"line": 1,"column": 32}],"path": ["launch"],"extensions": {"code": "INTERNAL_SERVER_ERROR"}}],"data": {"launch": null}}
Which will produce this error message in the UI when tapping on a launch:
This is all good! You can inspect the errors
field to add more advanced error management.
Restore the correct launch ID: LaunchDetailsQuery(launchId)
before moving on.
Practice
errors
field, which of the following statements are true?data
and errors
.Up next
Queries are only the beginning — now it's time to look at the operations that allow us to change data, mutations!
Share your questions and comments about this lesson
This course is currently in
You'll need a GitHub account to post below. Don't have one? Post in our Odyssey forum instead.