New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
RFC: v4 error handling #4711
Comments
I personally feel that this is a burden that Apollo should handle and shouldn't be pushed onto library consumers. There's two cases you mention above -- I would propose that Apollo handle these internally as values. CacheMissException is the clearly odd one out here -- I personally don't think it's exceptional for a cache to be lacking a value. I feel it should be modeled like so: internal sealed interface CacheResult<T> {
object None : CacheResult<Nothing>
class Some(val data: T) : CacheResult<T>
} Then when dealing with cache misses, exposing the flow is simply a case of calling I don't currently know if it's possible for a cache value to be nullable or not, but that might simplify this even further. No need to model is as a sealed hierarchy if so.
For network exceptions this is what I expect. Hoisting this functionality into a If handling network exceptions is painful from an Apollo library maintenance standpoint I'd expect Apollo to model them as a value internally and rethrow them at places where a library consumer interacts with them. I don't personally use any fetch policies aside from |
For what its worth, I generally try and model exceptions for uses which are exceptional. I don't try to model exceptions when a user or consumer of a library has done something incorrectly. However, the following point made resonates with me quite a bit.
We experienced this issue and actually leaned the opposite way. We created our own sealed class CustomException(
message: String? = null,
cause: ApolloException? = null,
) : RuntimeException(message, cause)
class CustomNetworkException(
message: String? = null,
cause: ApolloException? = null,
) : CustomException(message, cause)
class SemanticException(
val semanticError: SemanticError,
message: String,
) : CustomException(message, null)
suspend fun <D: Operation.Data> ApolloCall<D>.executeCustomException(): D {
try {
val response = execute()
if (response.hasErrors()) {
throw SemanticException(response.getSemanticError())
} else {
return response.dataAssertNoErrors
}
} catch (e: ApolloException) {
throw when (e) {
is ApolloNetworkException,
is ApolloHttpException,
is ApolloWebSocketClosedException -> CustomNetworkException(cause = e)
else -> e
}
}
}
private fun Response<*>.getSemanticError(): SemanticError { /* omitted */ } I like this because it's pragmatic and allows us to have one paradigm for handling errors. I dislike this because it's not "pure" from an exceptions standpoint. At use site it looks something like this: suspend fun doAThing() = try {
client.query(/*omitted*/).executeWithCustomException()
} catch (e: SemanticException) {
// do something with the semantic error.
} catch (e: CustomNetworkException) {
// do something
} |
Can you elaborate a bit more? Currently everything is an
The idea is that you need to check for val data = someResponse.data
if (data != null) {
// woohoo
println(data.foo.bar)
} else {
println("woops...)
} At least you're forced to check nullability (or force unwrap but then there's not much we can do) while exception catching get forgotten a lot. The Google Play SDK Index retrieves the crashes with apollo in the stack trace and this is #1 by very very far.
Things get really hairy if someone does this: apolloClient.query(query).fetchPolicy(CacheFirst).refetchPolicy(NetworkOnly).watch().collect {
// stuff
} That's a use case if you need to refresh your data when something changes in your cache. |
How can I handle exception when using |
I like how errors are being handled by V4 so far (4.0.0-beta.2), however, I'm interested to know why ApolloGraphQLException.errors has been deprecated in 4.0.0-beta.3? |
@LiamDeGrey In beta.3, We changed our mind from always setting an exception on GraphQL errors because that breaks the partial data use cases and this has bitten a few early 4.0 adopters (and forced us to add very weird code too). Can you share more about your use case? There might be ways to address it differently. |
Error handling is an important aspect of a client library and as we’re looking at 4.0, we think it can be improved and are seeking feedback.
Exceptions vs GraphQL errors
2 kinds of errors need to be handled:
errors
fieldCurrent situation
Currently, non-GraphQL errors are surfaced by throwing exceptions, whereas GraphQL errors are returned in
ApolloResponse.errors
. (See v3 doc error section).Some drawbacks:
Proposed change
Similarly to
ApolloResponse.errors
, a new fieldApolloResponse.exception: ApolloException
is added, and the response is emitted (.toFlow()
) or returned (.execute()
), instead of throwing.execute()
:.toFlow()
:This addresses the drawbacks above, and arguably is a more consistent API - at the cost of a breaking change. To ease the transition, methods that throw like in 3.x will be added. Projects will be able to use them temporarily to keep the old behavior and migrate to the new one progressively.
Partial data and nullability
In the area of error handling, pain points related to handling GraphQL errors and nullability have also been identified:
null
(only in error cases)To improve usability around this, v4 will introduce:
data
is guaranteed null when there are errors / non-null otherwise@catch
andFieldResult
to handle partial data for specific fields@semanticNonNull
to make fields generated as non nullThis is introduced in #5405, and documentation is in #5407.
Feedback
Please comment on this ticket with any feedback you may have!
The text was updated successfully, but these errors were encountered: