With 4.x error handling, I wanted to see if it's p...
# apollo-kotlin
s
With 4.x error handling, I wanted to see if it's possible to make my flows for FetchPolicy.CacheAndNetwork do this: β€’ Look at cache, if there is something, emit that. If there is a cache miss (which as I understand now means exception is of type
CacheMissException
), do not emit anything and just continue with the network request β€’ Then do network request, and for this if it succeeds emit the success, otherwise if it fails emit the failure so that the caller actually knows that both have failed. So this would be kinda like the normal CacheFirst option, but with the change that it does not stop at only the cache if the cache does in fact respond with something. If I make this function as an extension to
ApolloCall<D>
I got no way to do this safely in a way that would work for other FetchPolicies right? Because if I just do not emit anything on a cache miss, if someone had passed CacheOnly for example, then the flow would simply emit nothing at all and the caller would not see a result ever. My use case is that in some screen, I want to get the cached value if it's available to show it immediately in the UI. Then I want to ALSO fetch from the network because we've had some stale cache issues there. But I do not want to show the "error" state in-between while I just looked at the cache and it returned no data, and I am now waiting for the network request. I would prefer to just keep it at it's "Loading" state, thinking it has still just not received anything back yet.
I feel like I could make this work if I knew for a fact that my FetchPolicy is specifically CacheAndNetwork, so I know that even if I emit nothing from the cache, the network will for sure come afterwards.
My first thought was perhaps doing something silly like looking at
this.executionContext.get(FetchPolicyContext)
To try and manually check, but that's internal, probably for good reason πŸ˜„ Perhaps I could make a specialized extension function on ApolloCall<D> which both sets the policy to CacheAndNetwork, and then does something like this: But that would also look odd from the caller, as it'd look different from all other places where we do fetch policy separate from the rest
Oh there is
isLast
which I could perhaps use instead? πŸ‘€πŸ‘€πŸ‘€ If I know that it's in fact the last emission, I can just emit the cache miss as-is, to inform about the failure! If it's false, I can safely(?) just emit nothing, since I know there's gonna be more emissions coming later, where those can in fact emit the failure if that also fails.
The docs for isLast say:
Copy code
There can be false negatives where [isLast] is false if the producer does not know in advance if
other items are emitted. For an example, the CacheAndNetwork fetch policy doesn't emit the network
item if it fails.

There must not be false positives. If [isLast] is true, no other items must follow.
I am not 100% sure what it means by "the CacheAndNetwork fetch policy doesn't emit the network item if it fails.". If the cache fails it emits the cache failure, then it proceeds to continue returning all the network responses. Is it that those network responses may never come for some reason? What reason would that be?
b
> Perhaps I could make a specialized extension function on ApolloCallD which both sets the policy to CacheAndNetwork, and then does something like this This sounds reasonable to me. Ignoring cache misses or not is something that will depend on the use case. And using CacheAndNetwork or another policy also depends. They go well together πŸ™‚
s
Yeah I might do that in the end. I am just dabbling in learning the new error model without exceptions and all, so this is a good learning experience. For my last question here https://kotlinlang.slack.com/archives/C01A6KM1SBZ/p1721309923284229?thread_ts=1721309174.227279&amp;cid=C01A6KM1SBZ, do you think you know of examples of scenarios where in
CacheAndNetwork
I will get the cache miss emission, with
isLast = false
and then I will not get any more emissions ever? I am still having a hard time figuring out when that would be the case. Why would it not emit again if there is a failure in the network exception, is that how it always behaves? The code here https://github.com/apollographql/apollo-kotlin/blob/e3f72aade9a4f7cd1aa6e5d7e7c688[…]pollographql/apollo/cache/normalized/FetchPolicyInterceptors.kt just forwards the request as-is in
chain.proceed(request)
, after it's done trying to hit the cache. Why would that not emit anything afterwards?
b
hmm no I can't really think of such case, I think you're right that it should emit always - this comment probably predates the change with
.exception
instead of throwing
s
If that is the case and I don't need to worry about this, I might be able to make a general thing for any fetchpolicy then. If I can safely rely on isLast being false meaning that I should expect a new emission to come later anyway, so I am safe to just emit nothing on a cache miss.
b
worth a try πŸ™‚ Don't hesitate to let us know how that goes!
thank you frog 1
s
After looking into this a bit again, I am for now going with this approach. All my queries return back an
Either<ApolloOperationError, Data>
, where
ApolloOperationError
is this
Copy code
sealed interface ApolloOperationError {
  val throwable: Throwable?

  fun isCacheMiss(): Boolean = this is CacheMiss

  data class CacheMiss(override val throwable: CacheMissException) : ApolloOperationError {
    override fun toString(): String {
      return "CacheMiss(throwableMessage=${throwable.message}, throwable=$throwable)"
    }
  }

  data class OperationException(override val throwable: ApolloException) : ApolloOperationError {
    override fun toString(): String {
      return "OperationException(throwableMessage=${throwable.message}, throwable=$throwable)"
    }
  }

  data class OperationError(private val message: String) : ApolloOperationError {
    override val throwable: Throwable? = null
  }
}
Which is created from this code, which I've tried to write following the information from the truth table. Which btw was insanely helpful for me, thanks so much! The one thing we don't handle there is partial responses (data + errors) but we never did so I am leaving it as-is for now. And then on each call site, if I know that I do not want to show the temporary error while waiting for the network to respond, I just check for
isCacheMiss
and ignore that. Works well for me so far. It's a bit manual work on the call-site, but at the same time I am not sure if I want to introduce something more involved than this. I will come back here if we ever change this more πŸ˜„
b
This LGTM πŸ‘. One nitpick naming preference is I would name
OperationException
->
FetchError
and
OperationError
->
GraphQLError
. The names you picked are also good since they match what we have in the library.
thank you color 1
s
Hmm yeah that's a good idea. I like the GraphQLError idea. For the exception I was thinking of keeping the word "exception" in there, to kinda make ourselves understand that there was some exception thrown there. I want to in the future expand this a bit as per the discussion here https://kotlinlang.slack.com/archives/C01A6KM1SBZ/p1722933115952349?thread_ts=1722898667.911759&amp;cid=C01A6KM1SBZ to perhaps make more specific types of errors for things that we know we should be able to recover from, or things that we do not necessarily want to care about, and instead just ignore. In any case, I am feeling quite more comfortable now with how we handle all errors and exceptions, this 4.x update is super nice about this! No more try catches also makes me feel more confident that we are not swallowing things that we perhaps shouldn't be.
I have also an extension like
Copy code
fun <D : Operation.Data> Flow<Either<ApolloOperationError, D>>.filterCacheMisses(): Flow<Either<ApolloOperationError, D>> {
  return this.filterNot { it.leftOrNull()?.isCacheMiss() == true }
}
Which would also work fine, if I want the filtering to happen higher up, but haven't decided if I like this or not yet πŸ˜„
πŸ‘€ 1
πŸ‘ 1
b
feeling quite more comfortable now with how we handle all errors and exceptions, this 4.x update is super nice about this!
so good to hear that! πŸŽ‰
keeping the word "exception" in there, to kinda make ourselves understand that there was some exception thrown there.
yes probably a good idea