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@336b4d09))
// 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