elixir

Tracking Errors in Absinthe for Elixir with AppSignal

Aestimo Kirina

Aestimo Kirina on

Tracking Errors in Absinthe for Elixir with AppSignal

GraphQL provides a powerful approach to building APIs, and Absinthe is the leading GraphQL implementation for Elixir applications. While GraphQL offers many benefits, it can introduce a set of errors and performance bottlenecks that might be challenging to track and debug.

In this article, you’ll learn how to use AppSignal to monitor, debug, and resolve errors in your Absinthe-based GraphQL API.

To follow along with the tutorial, you'll need a few things in place.

Prerequisites

Introducing Our Example Phoenix App

Our example application is a Phoenix app that models a simple sports league management system with leagues, seasons, teams, and players. The API is accessible via a GraphQL endpoint at /wendigo/api/graphql, and a GraphiQL interface at /wendigo/api/graphiql (a nifty web interface for testing the API in development).

Going through a step-by-step project build would make this tutorial too long. So, from here onwards, we'll be referencing our pre-built example app, which already has Absinthe integrated. For more information on integrating Absinthe into a new Phoenix application, see the documentation.

Next, let's add the AppSignal mix package.

Adding the AppSignal Package

Open up the mix.exs file and add the main AppSignal package, as well as the AppSignal Phoenix package (required for Phoenix apps):

Elixir
# mix.exs ... defp deps do [ ... {:appsignal, "~> 2.8"}, {:appsignal_phoenix, "~> 2.0"} ... ] end ...

Now run mix deps.get to fetch the newly added dependencies.

Installing and Configuring AppSignal

Run mix appsignal.install YOUR_PUSH_API_KEY to install AppSignal, then go through the configuration options that will be presented to you:

  • Name your app: Give your app a suitable name.
  • The configuration method to be used: Here, you have a choice between using an environment variable or using a config file. I recommend using the configuration file, as it provides more flexibility later on.

Once you're done with the configuration, you'll see an output similar to the one below, which means you've successfully configured AppSignal:

Shell
Welcome to AppSignal! This installer will guide you through setting up AppSignal in your application. We will perform some checks on your system and ask how you like AppSignal to be configured. ================================================================================ Validating Push API key: Valid! 🎉 What is your application's name? [wendigo]: wendigo_app There are two methods of configuring AppSignal in your application. Option 1: Using a "config/appsignal.exs" file. (1) Option 2: Using system environment variables. (2) What is your preferred configuration method? [1]: 1 Writing config file config/appsignal.exs: Success! Linking config to config/config.exs: Success! Activating dev environment: Success! Activating prod environment: Success! AppSignal detected a Phoenix app ...

Once that's done, the app should start sending data over to the AppSignal dashboard:

Initial AppSignal Dashboard

Before moving into the actual implementation of error tracking in Absinthe, it's very important that we learn what GraphQL is all about, and especially how it works. This will help us understand where errors could come from and give us a basis for setting up effective error tracking.

What Is GraphQL?

Nowadays, building applications often involves providing API access points. REST APIs are often used as the solution for serving data to clients, but creating a REST API that meets all user requirements can be challenging, especially as applications grow in complexity.

And that's where GraphQL comes in. Unlike REST, GraphQL empowers clients to request the exact data they need.

We won't go into the details of how GraphQL actually works; instead, we'll focus on the GraphQL schema since it's the interface concerned with processing GraphQL requests. Later in this article, we'll learn how errors occur and how to resolve them using AppSignal.

The GraphQL Schema

The GraphQL schema is a blueprint of the entire API. It helps us define the domain model and how the server will respond to requests for data.

Below is summarized code showing the example application schema:

Elixir
#lib/wendigo_web/graphql/schema.ex defmodule WendigoWeb.GraphQL.Schema do use Absinthe.Schema alias Wendigo.Context.{Leagues, Players, Seasons, Teams} alias WendigoWeb.Resolvers.{LeagueResolver, PlayerResolver, SeasonResolver, TeamResolver} import_types(Absinthe.Type.Custom) import_types(WendigoWeb.GraphQL.Types) query do @desc "Get a league" field :league, :league do arg(:id, non_null(:id)) resolve(&LeagueResolver.get_league/3) end @desc "List leagues" field :leagues, list_of(:league) do arg(:first, :integer) arg(:offset, :integer) resolve(&LeagueResolver.list_leagues/3) end ... end mutation do @desc "Create a season" field :create_season, :season do arg(:name, non_null(:string)) arg(:start_date, non_null(:date)) arg(:end_date, non_null(:date)) resolve(&SeasonResolver.create_season/3) end ... end end

The schema is made up of several key components: types, queries, and mutations.

Types

Types describe the shape of the data objects. Types can be used directly in the schema or imported from elsewhere to keep the schema clean and organized.

The code below shows the league object type:

Elixir
# lib/wendigo/schema/league.ex defmodule Wendigo.Schema.League do @moduledoc """ Schema for the `leagues` table. """ use Ecto.Schema import Ecto.Changeset @type t() :: %__MODULE__{} @primary_key {:id, :string, autogenerate: {Ecto.Nanoid, :autogenerate, []}} schema "leagues" do field :name, :string field :level, :string timestamps() end @doc "Creates a changeset for a league" def changeset(struct, params) do struct |> cast(params, [:name, :level]) |> validate_required([:name]) |> validate_length(:name, max: 100) |> validate_length(:level, max: 100) end def changeset(params), do: changeset(%__MODULE__{}, params) end

Queries

These are read-only operations that fetch data.

Elixir
... query do @desc "Get a league" field :league, :league do arg(:id, non_null(:id)) resolve(&LeagueResolver.get_league/3) end end ...

Mutations

Mutations are operations that modify data:

Elixir
... mutation do @desc "Create a season" field :create_season, :season do arg(:name, non_null(:string)) arg(:start_date, non_null(:date)) arg(:end_date, non_null(:date)) resolve(&SeasonResolver.create_season/3) end ... end ...

With the information we have so far, let's now explore some of the errors you might encounter when working with Absinthe and how you can use AppSignal to track them.

Absinthe Errors

There are many error types that can affect a GraphQL API, including:

  • Schema validation errors - These happen when the query doesn't conform to the rules defined in the schema (for example, requesting a field that doesn't exist in the schema):
GraphQL
{ user(id: "123") { nonexistentField } }
  • Query syntax errors happen when a client sends a malformed query, (for example, an incorrect query syntax, as shown below):
GraphQL
{ user(id: "123" { # Missing closing parenthesis name } }
  • Resolver errors can occur when a resolver fails to fetch data for one reason or another. (For example, failing to find a record or mishandling nil values):
GraphQL
resolve fn %{id: id}, _ -> case Repo.get(User, id) do nil -> {:error, "User not found"} user -> {:ok, user} end end
  • Type mismatch errors occur when the data returned by a resolver fails to match what is defined by the schema. A good example is returning a string instead of an integer:
GraphQL
field :age, :integer do resolve fn _, _ -> {:ok, "twenty three"} end end
  • Complexity limit errors: Absinthe allows you to set up limits on the number of levels when deep nesting a query. As such, should a request exceed the set limits, a complexity limit error occurs.
GraphQL
{ users { orders { items { product { supplier { contact { phone } } } } } } }

These aren't the only errors you can expect; there are others like authentication (and authorization) errors, and argument validation errors. You can get more information about them from the GraphQL error documentation.

Now that you know the types of errors to expect, let's see how we can use AppSignal to handle a few of them.

Tracking Absinthe Errors Using AppSignal

By default, AppSignal will automatically instrument Absinthe requests, as you can see below:

Automatic Absinthe Tracking

However, for Absinthe-specific errors, you'll need an extra setup to effectively surface these errors to the AppSignal dashboard.

How Absinthe Treats Errors

Absinthe doesn't crash when resolvers return an error; instead, it includes these errors in the GraphQL response with a 200 HTTP status code.

You can see this in the example shown below, where I made a query to create a league and intentionally failed to include the required name field:

Graceful Error Handling

As you can see, returning errors as tuples allows the developer to handle expected failures whichever way they wish, without crashing the request flow.

For an exception to be raised in Absinthe, the following conditions need to be met:

  • Having actual bugs in the code. For example, undefined function modules and so forth.
  • Database or Ecto errors that are not handled at the context level.
  • Absinthe middleware errors.

These are not necessarily the only conditions, but they represent a big percentage of what would cause Absinthe to raise exceptions.

With that in mind, let's use an Absinthe middleware to set up a custom instrumentation and catch a resolver error that would have otherwise gone under the radar.

Custom Instrumenting Absinthe Errors

Let's assume we want to surface a resolver error when a user tries to fetch a team without a valid ID.

The relevant get_team/3 resolver function is shown below:

Elixir
defmodule WendigoWeb.Resolvers.TeamResolver do alias Wendigo.Context.Teams alias WendigoWeb.Error def get_team(_parent, %{id: id}, _resolution) do case Teams.get(id) do nil -> {:error, "Team not found: #{id}"} team -> {:ok, team} end end ... end

But like I mentioned earlier, this wouldn't show up as an error on AppSignal since Absinthe would still return a 200 status code. To make sure the error is surfaced as we want, we need to set up an Absinthe middleware.

Absinthe Middlewares

In Absinthe, a middleware functions as a layer that processes GraphQL requests as they flow through an application, allowing you to perform tasks like error handling, logging, authorization checks, or data transformation before or after resolvers execute.

For the purpose of error tracking, middlewares are particularly valuable because they can intercept and process any errors that occur during field resolution, whether they're from failed database queries, validation issues, or other runtime problems.

With that in mind, let's set up a middleware called ErrorReporter to handle resolution errors:

Elixir
# lib/wendigo_web/middleware/error_reporter.ex defmodule WendigoWeb.Middleware.ErrorReporter do @behaviour Absinthe.Middleware def call(resolution, _config) do case resolution.errors do [] -> resolution errors -> if span = Appsignal.Tracer.current_span() do formatted_errors = Enum.map(errors, &format_error/1) error_message = Enum.join(formatted_errors, ", ") error = %RuntimeError{ message: error_message } Appsignal.Span.add_error( span, error, %{ graphql_error: true, validation_error: true } ) Appsignal.Span.set_sample_data(span, "graphql", %{ query: resolution.definition.name || "anonymous", path: Absinthe.Resolution.path(resolution) }) end resolution end end defp format_error(%{message: message}), do: message defp format_error(error), do: inspect(error) end

Then, edit the schema to include this middleware:

Elixir
# lib/wendigo_web/graphql/schema.ex defmodule WendigoWeb.GraphQL.Schema do ... # Middleware def middleware(middleware, _field, _object) do middleware ++ [WendigoWeb.Middleware.ErrorReporter] end end

Now let's see how this works:

  • The ErrorReporter middleware is added to all schema operations through the middleware/3 function shown in the code snippet above.
  • When get_team/3 is called, the middleware receives a resolution object for each GraphQL field. If there are no errors, the resolution proceeds.
  • If there are errors, the middleware formats each error as a string; multiple errors are joined together using commas and sent to AppSignal using the current span.

Having mentioned AppSignal's "span", here's what it is and how it works:

  • Appsignal.Span represents a single unit of work in a tracing call. The add_error() function attaches error information to the span as well as any metadata tags that might be available. In the same token, the set_sample_data() function is used to add context about a GraphQL request to enrich the data that will appear on AppSignal when the span data is sent.
  • Appsignal.Tracer is used for tracking the request flow in the app. The current_span() function returns the active trace span if it's available.

Now, whenever a resolver is executed, the middleware will run with the ability to catch and report errors from field resolvers, input validation, schema validations, runtime errors, and so forth.

A good example of middleware in action is shown in the screenshots below, which highlight a resolution error that has been reported in the AppSignal dashboard:

Resolver Error

There's more detail about the error here:

Resolver Error Details

Wrapping Up

In this article, we've explored how to effectively track and monitor errors in an Absinthe GraphQL API using AppSignal. We've covered the various types of GraphQL errors you might encounter, from schema validation to resolver errors, and demonstrated how to set up custom error tracking using Absinthe middleware.

Happy coding!

Wondering what you can do next?

Finished this article? Here are a few more things you can do:

  • Share this article on social media
Aestimo Kirina

Aestimo Kirina

Our guest author Aestimo is a full-stack developer, tech writer/author and SaaS entrepreneur.

All articles by Aestimo Kirina

Become our next author!

Find out more

AppSignal monitors your apps

AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!

Discover AppSignal
AppSignal monitors your apps