
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
- An AppSignal account — you can sign up for a free trial.
- Some experience working with Elixir and the Phoenix framework.
- A local installation of Elixir, the Phoenix framework, and PostgreSQL. Follow this guide to get your local environment set up.
- An Elixir app with Absinthe integrated. If you don't have one ready, fork the example app we'll be using for this tutorial.
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):
# 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:
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:

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:
#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:
# 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.
... 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:
... 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):
{ user(id: "123") { nonexistentField } }
- Query syntax errors happen when a client sends a malformed query, (for example, an incorrect query syntax, as shown below):
{ 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):
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:
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.
{ 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:

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:

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:
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:
# 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:
# 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 themiddleware/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. Theadd_error()
function attaches error information to the span as well as any metadata tags that might be available. In the same token, theset_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. Thecurrent_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:

There's more detail about the error here:

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:
- Subscribe to our Elixir Alchemy newsletter and never miss an article again.
- Start monitoring your Elixir app with AppSignal.
- Share this article on social media
Most popular Elixir articles
A Complete Guide to Phoenix for Elixir Monitoring with AppSignal
Let's set up monitoring and error reporting for a Phoenix application using AppSignal.
See moreEnhancing Your Elixir Codebase with Gleam
Let's look at the benefits of using Gleam and then add Gleam code to an Elixir project.
See moreUsing Dependency Injection in Elixir
Dependency injection can prove useful in Elixir. In this first part of a two-part series, we'll look at some basic concepts, core principles, and types of dependency injection.
See more

Aestimo Kirina
Our guest author Aestimo is a full-stack developer, tech writer/author and SaaS entrepreneur.
All articles by Aestimo KirinaBecome our next author!
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!
