I want to share my approach to handling errors in GraphQL resolvers (I use GraphQL Yoga on the server-side) and pass them to the frontend side (where I use Svelte + @urql/svelte).
Backend
Packing error info
As I figured out, the only way to return an error from a GraphQL endpoint is by throwing an error in a resolver. That design imposes some restrictions on transferring error data between the backend and frontend. Let's imagine some bad request came that the server can't process, and you want to return an error and probably, some details about what exactly went wrong. Suppose that we want to pack all info in the following interface:
type ApiError = {
code: 400,
message: "The request was bad :("
}
Back to restrictions - there is no way to put something object-like as an argument into the new Error(arg)
call, but we can transform the ApiError
object into a JSON string. That is the weird little trick that allows packing any information into a throwable. That's all about information transfer, but there are a couple more things about code organization.
Return error from GraphQL endpoint by throwing error could be convenient and a type safe with Typescript assert functions. There is no need to wrap return statements into if
statements; just throw an error and keep a flat code structure. Let's combine that with packing information into an error object:
export const newApiError = (errorObject: ApiError) => {
return new Error(JSON.stringify(errorObject))
}
Now it's possible to do throw newApiError(apiError)
.
But it's still necessary to check whether we need to throw, which means if
statements. Could we do better? Yes, with typescript assert functions.
Getting rid of if
statements
Imagine that a backend receiving a request on an endpoint requires authorization, but there are no creds in the request. It looks like we want to say, "401: Not authorized, there are no creds, you shall not pass." So, what is the easiest way to do it in resolver? I suppose that the following:
// get an auth token somehow
const authToken string | null = req.headers.get("Authorization")
assert(
authToken,
"401: There are no creds, you shall not pass"
)
Custom assert
But it's the only string; as we are not allowed to pass anything like an object into an assert call, so let's write our realization of assert:
export function assertWithApiError<T>(
statement: T | null | undefined,
errorObject: ApiError
): asserts statement is T {
if (!statement) {
throw newApiError(errorObject)
}
}
Typescript assert function allows us to have typesafe assertions in which we can throw any throwable even created by ourselves, with any data packed into error.
// get an auth token somehow
const authToken: string | null = req.headers.get("Authorization")
assertWithApiError(
authToken,
{ message: 'Auth token is not presented', code: 401 }
)
// there is typescript knows that token is a string
Frontend
What about a frontend part? As I said above, I use @svelte/urql package for dealing with GraphQL in my svelte app. Let's look at the code!
Start with the common @svelte/urql use case:
// mutation function is from @svelte/urql package
const editUserMutation = mutation<{ me: User }>({
query: EDIT_USER,
})
const { data, error } = await editMeMutation({ intro })
So, the error there is CombinedError
type from @svelte/urql, which includes an array of GraphQL original errors:
type OriginalError = {
message: string
stack: string
}
export const parseGraphqlApiErrors = (error: CombinedError): ApiError[] => error.graphQLErrors.map((e) => {
const rawOriginalError = (e.extensions?.originalError as OriginalError).message
return parseApiError(rawOriginalError)
})
export const parseApiError = (jsonString: string): ApiError => {
try {
const parsed: unknown = JSON.parse(jsonString)
if (apiErrorTypeGuard(parsed)) {
return parsed
} else {
throw new Error('got invalid api error')
}
} catch (e) {
console.error(e)
throw Error("Can't parse api error from json string")
}
}
const apiErrorTypeGuard = (possiblyError: any): possiblyError is ApiError =>
typeof possiblyError === 'object' && 'code' in possiblyError && 'message' in possiblyError
At the end of the story, we have custom assertions that are throwing errors stuffed with any data we want and frontend code that can extract that data; that's it.
I'm not a very experienced user of GraphQL, Svelte, or Urql, and I would be happy if you point me to any existing solution which is better than that described above and hope my ideas will come in handy for someone :)
Photo by Mario Mendez
Top comments (0)