Skip to main content

Error handling

There are different types of errors in gql.

  • Schema validation errors, which should be caught in development. These are for instance caused by duplicate field names or invalid typenames.
  • Query preparation errors, which are errors caused by invalid queries.
  • Execuion errors. These are errors that occur during query evaluation, caused by resolvers that fail.

Execution​

Error handling in gql can be performed in two ways, it can be returned explicitly or raised in F.

Examples​

Let's setup the scene:

import gql.ast._
import gql.dsl.all._
import gql.dsl.all.value._
import gql._
import cats.implicits._
import cats.data._
import cats.effect._
import cats.effect.unsafe.implicits.global
import io.circe.syntax._

def multifailSchema =
tpe[IO, Unit](
"Query",
"field" -> build.from(arged(arg[Int]("i", scalar(10))).evalMap{
case 0 => IO.pure(Ior.left("fail gracefully"))
case 1 => IO.raiseError(new Exception("fail hard"))
case i => IO.pure(Ior.right(i))
}.rethrow)
)

def go(query: String, tpe: Type[IO, Unit] = multifailSchema) =
Schema.query(tpe).flatMap { sch =>
Compiler[IO].compile(sch, query) match {
case Left(err) =>
println(err)
IO.pure(err.asJson)
case Right(Application.Query(fa)) =>
fa.map{x => println(x.errors);x.asJson }
}
}.unsafeRunSync()

go("query { field }")
// Chain()
// res0: io.circe.Json = JObject(
// value = object[data -> {
// "field" : 10
// }]
// )

A query can fail gracefully by returning Ior.left:

go("query { field(i: 0) }")
// Chain(Error(Right(fail gracefully),Chain("field")))
// res1: io.circe.Json = JObject(
// value = object[data -> {
// "field" : null
// },errors -> [
// {
// "message" : "fail gracefully",
// "path" : [
// "field"
// ]
// }
// ]]
// )

A query can fail hard by raising an exception:

go("query { field(i: 1) }")
// Chain(Error(Left(java.lang.Exception: fail hard),Chain("field")))
// res2: io.circe.Json = JObject(
// value = object[data -> {
// "field" : null
// },errors -> [
// {
// "message" : "internal error",
// "path" : [
// "field"
// ]
// }
// ]]
// )

A query can also fail before even evaluating the query:

go("query { nonExisting }")
// Preparation(Chain(PositionalError(Cursor(Chain()),List(Caret(0,8,8)),Field 'nonExisting' is not a member of `Query`.)))
// res3: io.circe.Json = JObject(
// value = object[errors -> [
// {
// "message" : "Field 'nonExisting' is not a member of `Query`.",
// "locations" : [
// {
// "line" : 0,
// "column" : 8
// }
// ]
// }
// ]]
// )

And finally, it can fail if it isn't parsable:

def largerQuery = """
query {
field1
field2(test: 42)
}

fragment test on Test {
-value1
value2
}
"""

go(largerQuery)
// Parse(ParseError(Caret(8,4,80),cats.Always@705aa045))
// res4: io.circe.Json = JObject(
// value = object[errors -> [
// {
// "message" : "could not parse query",
// "locations" : [
// {
// "line" : 8,
// "column" : 4
// }
// ],
// "error" : "\u001b[34mfailed at offset 80 on line 7 with code 45\none of \"...\"\nin char in range A to Z (code 65 to 90)\nin char in range _ to _ (code 95 to 95)\nin char in range a to z (code 97 to 122)\nfor document:\n\u001b[0m\u001b[32m| \u001b[0m\u001b[32m\n| query {\n| field1\n| field2(test: 42)\n| }\n| \n| fragment test on Test {\n| \u001b[41m\u001b[30m-\u001b[0m\u001b[32mvalue1\n| \u001b[31m>^^^^^^^ line:7, column:4, offset:80, character code code:45\u001b[0m\u001b[32m\n| value2 \n| }\n| \u001b[0m\u001b[0m"
// }
// ]]
// )

Parser errors also look nice in ANSI terminals:

Terminal output

Exception trick​

If for whatever reason you wish to pass information through exceptions, that is also possible:

final case class MyException(msg: String, data: Int) extends Exception(msg)

val res =
Schema.query(
tpe[IO, Unit](
"Query",
"field" -> eff(_ => IO.raiseError[String](MyException("fail hard", 42)))
)
).flatMap { sch =>
Compiler[IO].compile(sch, "query { field } ") match {
case Right(Application.Query(run)) => run
}
}.unsafeRunSync()
// res: QueryResult = QueryResult(
// data = object[field -> null],
// errors = Singleton(
// a = Error(
// error = Left(value = MyException(msg = "fail hard", data = 42)),
// path = Singleton(a = JString(value = "field"))
// )
// )
// )

res.errors.headOption.flatMap(_.error.left.toOption) match {
case Some(MyException(_, data)) => println(s"Got data: $data")
case _ => println("No data")
}
// Got data: 42