Skip to content
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

Deprecate ApolloCompositeException with Error level and use suppressed exceptions instead #4062

Closed
Tracked by #4171
WildOrangutan opened this issue Apr 25, 2022 · 16 comments · Fixed by #4718
Closed
Tracked by #4171

Comments

@WildOrangutan
Copy link
Contributor

Use case
I'm finding ApolloCompositeException annoying to handle. It contains multiple exceptions and you kind of have to pick one exception, and based on it show error message to user. Along side that, you have to potentially handle and report all other errors (e.g. Firebase).

For example:

try {
    graphqlService.mutate(Whatever())
} catch(e: ApolloCompositeException) {
    when (val causes = e.suppressedExceptions) {
        causes.any { it is ApolloNetworkException } -> TODO("handle network")
        causes.any { it is HttpCacheMissException } -> TODO("handle cache")
        else -> TODO("handle other")
    }
}

Describe the solution you'd like
I may be missing some context, why it was designed in such a manner. But ideally, I would prefer smth like:

try {
    graphqlService.mutate(Whatever())
} catch(e: Exception) {
    when (e) {
        is ApolloNetworkException -> TODO("handle network")
        is HttpCacheMissException -> TODO("handle cache")
        else -> TODO("handle other")
    }
}
@martinbonnin
Copy link
Contributor

Hi ! 👋100% agree the exception handling could use some more ergonomic APIs. Not 100% about this specific case though. Mutations typically do not hit the cache so I don't think you should get an ApolloCompositeException ? Can you share the stacktrace when/if that happens?

@WildOrangutan
Copy link
Contributor Author

Ah, I see. I just wrote some random example, by using my imagination. So it probably only happens for querying then 🙂

@martinbonnin
Copy link
Contributor

I see 👍. For queries, the problem happens if you have both a network and a cache error. The caller might want to have access to the cache miss error fir an example. If you're not using the cache, you can simply ignore ApolloCompositeException

@WildOrangutan
Copy link
Contributor Author

I intend to use caching, so this exception is still relevant for me.

Is composite exception there only because of cache-miss? What if you exposed some sort of callback for cache-miss instead? Like cache-miss "interceptor", or smth along those lines?
I think if that would avoid composite exception, it would make exception handling less mind boggling. You will know better if that makes sense.

@martinbonnin
Copy link
Contributor

Is composite exception there only because of cache-miss?

Yes, mostly

I think if that would avoid composite exception, it would make exception handling less mind boggling

Let's say you're doing a CacheFirst query that fails because both cache and network fail. What exception would you expect there?

@WildOrangutan
Copy link
Contributor Author

In case of "cache miss callback", I'd say network exception.

But hm, in case of NetworkFirst this might get weird. ApolloCompositeException makes more sense when you take this into account.

Would it be possible to rather limit ApolloCompositeException a bit? By limit I mean set more specific type, that ApolloCompositeException can contain. Would make a lot of difference I think. For example changing type of exceptions: val suppressedExceptions: ApolloException.

When I first try to handle this exception, I had no clue what I can get in suppressedExceptions and it's still hard to predict what to expect. I had no idea, where to find "network exception". Someone could put another ApolloCompositeException into it, which was also something I was wondering if it's possible. At first I even thought, that I will directly get smth like UnknownHostException.

Hope you can see where I'm coming at :)

@martinbonnin
Copy link
Contributor

I 100% see the pain point there. Something we can do for sure is update the documentation (see #4065)

changing type of exceptions: val suppressedExceptions: ApolloException.

We can't do that because suppressedExceptions is built-in in kotlin.Throwable. But we could add something like:

// A helper function that returns the [suppressedExceptions] as a list of [ApolloException]
val suppressedApolloExceptions: List<ApolloException>.

Would that help? Also ping @BoD @akshay253101 since we had a related discussion in #3957, do you have any opinion on this?

@BoD
Copy link
Contributor

BoD commented Apr 27, 2022

In my opinion, yes, a suppressedApolloExceptions would be a nice to have, its presence kind of implicitly explains that the underlying exception can only be ApolloException (in fact we should probably also make this explicit in the doc).

Just for the sake of the argument (as we can't really change this now) I think the ideal would have been:

  • if there's only one exception, throw that
  • if there are 2, throw the 2nd one, but the 1st one is accessible through the standard suppressedExceptions (and clearly document that fact)

@WildOrangutan
Copy link
Contributor Author

From my pov, this would definitely better message what you can expect to handle. It would even better if we could somehow guarantee, that suppressedApolloExceptions won't contain ApolloCompositeException again :). Maybe also at least document that.

@akshay253101
Copy link
Contributor

ApolloCompositeException takes first and second exception as Throwable and added to suppressedException, So in constructor we can restrict it to ApolloException

@martinbonnin
Copy link
Contributor

martinbonnin commented Apr 27, 2022

if there's only one exception, throw that

This we do already, right?

if there are 2, throw the 2nd one, but the 1st one is accessible through the standard suppressedExceptions (and clearly document that fact)

I like this !

as we can't really change this now

Maybe we can? It's a behaviour change but also kind of an edge case so it should be transparent for the vast majority of users ? And it's not API/ABI breaking so there's no risk of a runtime exception from a transitive usage or such things.

For @akshay253101 use case (getting the network error from a Http NetworkFirst query), it means changing:

if (exception is ApolloCompositeException) {
   // get the network error
   exception.suppressedExceptions[0] as? ApolloException
}

to

if (exception.suppressedExceptions.isNotEmpty()) {
     // get the network error
     exception.suppressedExceptions[0] as? ApolloException
}

@BoD
Copy link
Contributor

BoD commented Apr 27, 2022

This we do already, right?

Changing the type of exception thrown would require clients who handle it to update the catch accordingly so I guess that's why I assumed we "can't" really do it 😅.

But it's true at least it shouldn't make the code crash - and of course we would clearly warn of the behavior change in the changelog. Might be a bit annoying in the short run, on the other hand this would allow deprecating ApolloCompositeException which seems to be confusing so that'd be for the best longer term.

@WildOrangutan
Copy link
Contributor Author

if there are 2, throw the 2nd one, but the 1st one is accessible through the standard suppressedExceptions (and clearly document that fact)

Hm, I also like the idea. I'm worried how annoying would it be to go "down the rabbit hole" to find exception, that you're looking for. Maybe some sort of helper method would make it bearable.

I guess it's not that bad, since it's only 2 atm. But it's again hard to predict how to handle it.

@martinbonnin
Copy link
Contributor

martinbonnin commented Apr 27, 2022

Changing the type of exception thrown would require clients who handle it to update the catch accordingly so I guess that's why I assumed we "can't" really do it 😅.

Maybe we deprecate ApolloCompositeException with Error level? This way the symbol is still there (no ABI breaking change) but it forces all callers to update explicitely? My favorite timing to do this would be at the same time as reworking the way we "throw" in general (see #4003). We could make a "error handling" release that simplifies the error handling a bunch

@BoD
Copy link
Contributor

BoD commented Apr 27, 2022

Maybe we deprecate ApolloCompositeException with Error level?

Totally 👍

We could make a "error handling" release that simplifies the error handling a bunch

Agreed as well

@martinbonnin martinbonnin changed the title Remove ApolloCompositeException? Deprecate ApolloCompositeException with Error level and use suppressed exceptions instead Apr 28, 2022
@BoD BoD mentioned this issue Nov 3, 2022
33 tasks
@martinbonnin
Copy link
Contributor

Fixed with #4718

I wend with throwing the first exception and adding all later ones as suppressed, similar to what would happen with try-with-resources

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants