3. Handle errors and partial data
8m

Overview

Handling error states is a crucial part of any quality user experience, and one of 's strengths is its ability to express unexpected behaviors with precision. 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
  • 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 , different types of errors can happen:

  • Fetch errors: connectivity issues, HTTP errors, JSON parsing errors, etc. In this case, response.exception contains an ApolloException. Both response.data and response.errors are null.
  • request errors: in this case, response.errors contains the errors. response.data is null.
  • field errors: in this case, response.errors contains the errors. response.data contains partial data.

Let's handle the first two for now: fetch errors and request errors.

First let's create a LaunchDetailsState sealed interface at the top of the module, just below the imports. This will encapsulate all possible states of the UI:

app/src/main/kotlin/com/example/rocketreserver/LaunchDetails.kt
private sealed interface LaunchDetailsState {
object Loading : LaunchDetailsState
data class Error(val message: String) : LaunchDetailsState
data class Success(val data: LaunchDetailsQuery.Data) : LaunchDetailsState
}

Then, add the sealed interface to your imports:

app/src/main/kotlin/com/example/rocketreserver/LaunchDetails.kt
import com.example.rocketreserver.LaunchDetailsState.Loading
import com.example.rocketreserver.LaunchDetailsState.Success
import com.example.rocketreserver.LaunchDetailsState.Error

Then, in LaunchDetails, examine the response returned by execute and map it to a State:

app/src/main/kotlin/com/example/rocketreserver/LaunchDetails.kt
@Composable
fun 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) data
LaunchDetailsState.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:

app/src/main/kotlin/com/example/rocketreserver/LaunchDetails.kt
// Use the state
when (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:

app/src/main/kotlin/com/example/rocketreserver/LaunchDetails.kt
@Composable
private fun LaunchDetails(data: LaunchDetailsQuery.Data) {
// everywhere within this function, replace `response?.data?.`
// with `data.`. Example:
// Mission patch
AsyncImage(
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 . You should see this:

Android Studio with a simulator open showing the error message "Oh no, an error happened"

This is good! We have successfully handled fetch and request errors. Now we will tackle GraphQL errors, which is often called "partial data."

Handle partial data

One of '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 , 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:

app/src/main/kotlin/com/example/rocketreserver/LaunchDetails.kt
state = when {
response.errors.orEmpty().isNotEmpty() -> {
// GraphQL error
Error(response.errors!!.first().message)
}
response.exception is ApolloNetworkException -> {
// Network error
Error("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 error
Error("Oh no... An error happened.")
}
}

For the purpose of this lesson, we're going to treat errors the same as request errors and won't attempt to render a UI with partial data. But and allow for significant flexibility on how to handle partial data and unexpected null values.

Let's test our new control flow. To trigger a error, replace LaunchDetailsQuery(launchId) with LaunchDetailsQuery("invalidId"). Disable airplane mode and select a . The server will send this response:

(response with a GraphQL field error)
{
"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 :

Android Studio with a simulator showing an error message

This is all good! You can inspect the errors to add more advanced error management.

Restore the correct ID: LaunchDetailsQuery(launchId) before moving on.

Practice

If a valid GraphQL response contains an errors field, which of the following statements are true?
True or false: a valid GraphQL response can include non-null values for both data and errors.

Up next

Queries are only the beginning now it's time to look at the that allow us to change data, !

Previous

Share your questions and comments about this lesson

This course is currently in

beta
. Your feedback helps us improve! If you're stuck or confused, let us know and we'll help you out. All comments are public and must follow the Apollo Code of Conduct. Note that comments that have been resolved or addressed may be removed.

You'll need a GitHub account to post below. Don't have one? Post in our Odyssey forum instead.