DEV Community

Cover image for End-to-end GraphQL error handling?
GF
GF

Posted on

End-to-end GraphQL error handling?

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 :("
}
Enter fullscreen mode Exit fullscreen mode

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))
}
Enter fullscreen mode Exit fullscreen mode

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"
)
Enter fullscreen mode Exit fullscreen mode

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)
    }
}

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 }) 
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

Oldest comments (0)