I am wondering how to shape the error handling for...
# apollo-kotlin
d
I am wondering how to shape the error handling for Apollo v4 in the light of upcoming rich errors, I can imagine that
ApolloResponse
will evolve to adopt it, and I would like to minimize the effort for the next migration. Of course it's very early to tell and maybe it's complicated from the Java interoperability perspective, but initially looking at the error handling, they seem pretty interesting
💡 1
m
Yea, I'm very much looking forward to rich errors
The root problem of all of this is GraphQL bonkers error handling and coalescing errors and nulls
I love GraphQL in general but that part is the worst, by far 😅
This is a bit of my life goal these days to fix this but it's going to take a bit of time
So to answer your question more directly, I'm dreaming of a world where ultimately the error is only 2 state: success or error
💯 1
🙌 1
TBH I'm not sure how that would look yet. Ideally GraphQL semantic nullability and rich errors land at the same time and that would be beautiful
d
just to clarify, as a response it's possible to get any of these results, right?
Copy code
- Full response
- Partial response + errors
- ApolloException
- ApolloException + errors
m
ApolloException + errors
That one is not possible
👍 2
d
I am wondering if I can leverage sealed types to avoid making all fields nullable
m
Yes, you can definitely map
If we were starting from scratch, we would have done this
It all depends how you want to handle partial errors
@catch
allows you to handle the errors in the query
If you don't care about partial data, you can also set
@catchByDefault(to: THROW)
and never have to check
.errors
ever
d
I guess that it can be acceptable in some cases but not as a general rule
still good to know these capabilities, thanks!
m
My current stance on this is that no partial data is a good default.
Some screens need to be more robust. Those screens need a specific UI to handle the partial errors.
So when writing the UI, you can use
@catch
to change some of your fields from
T
to
FieldResult<T>
But the error handling and the UI are ultimately coupled
Anyways, if you need more links let me know, this is my favorite topic in the world ^^
❤️ 1
s
d
perfect! based on this information I'll think how to model an intermediate type with valid responses that hopefully can help to avoid the 'all nullable' option and ease the transition to Rich Errors
🙌 1
maybe something like
Copy code
sealed class ServerResult<T> {
    data class Valid<T : Any>(
        val result: T,
        val errors: List<Error>?,
    ) : ServerResult<T>()

    data class Invalid<T>(
        val errors: List<Error>,
    ) : ServerResult<T>()

    class Failure<T>(
        val exception: ApolloException,
    ) : ServerResult<T>()
}
I need do think a bit how the different cases will be generally treated to shape it
m
It's
Valid
,
Partial
and
Failure
The
Invalid
up there isn't super useful I think?
d
for
Invalid
I am thinking of the row
Copy code
A GraphQL request error happened or a Graph field error bubbled up.
m
But then you will also have it in
Valid
if you put
errors
in
Valid
Or how would you decide between
Valid
and
Invalid
?
d
the idea behind
Valid
is that
T
has a value
👍 1
m
Ah yes, no data at all
That state is not much different from
Failure
though
semantically speaking
It's an error that prevents the display of any data
d
indeed, it could be merged and provide a generic treatment: • `Valid`: Proceed and display •
Invalid
: Report either exception or errors
m
Exactly
d
for alignment with the Truth table it could be
Copy code
sealed class ServerResult<T> {
    data class Valid<T : Any>(
        val result: T,
        val errors: List<Error>?,
    ) : ServerResult<T>()

    sealed class Invalid<T> : ServerResult<T>()

    class GraphFieldError<T>(
        val errors: List<Error>,
    ) : Invalid<T>()

    class RequestFailure<T>(
        val exception: ApolloException,
    ) : Invalid<T>()
}
but it will be more practical to merge the
Invalid
Copy code
sealed class ServerResult<T> {
    data class Valid<T : Any>(
        val result: T,
        val errors: List<Error>?,
    ) : ServerResult<T>()

    class Invalid<T>(
        val errors: List<Error>?,
        val exception: ApolloException?,
    ) : ServerResult<T>()
}
if it always has the same treatment
m
Yea I think so. From the point of view of the consumer, that's one case less to worry about
You could make a sub-sealed class I guess because
errors
and
exception
are exclusive in the
Invalid
case
Maybe a sealed hiearchy
Copy code
sealed interface Result
sealed class Valid: Result
sealed interface Invalid Result
sealed class RequestError: Invalid
sealed class IOError: Invalid
d
that's how the first example looks like
👍 1
m
Ah yes, sorry I missed that 🤦‍♂️
d
no problem
I am not sure if it's overkill, but this is what I came up with, let me know what you think https://gist.github.com/diego-gomez-olvera/1c3f420edc8218af01ea872f202effc9
I added it to the Rich Errors proposal to get feedback from their authors as well
kodee loving 1
this is unfortunate
Error types cannot have superclasses, superinterfaces, or generic parameters. Essentially, error types present a new flat hierarchy in the Kotlin type system. It's needed to form only disjoint unions.
sad panda 1