Handling GraphQL Errors in Flutter

When building Flutter applications that interact with GraphQL APIs, handling errors gracefully is crucial for providing a smooth user experience. GraphQL errors can occur due to various reasons, such as invalid queries, server-side issues, or network problems. This article provides a comprehensive guide on effectively handling GraphQL errors in Flutter, complete with code samples to illustrate each technique.

Understanding GraphQL Errors

GraphQL responses include an errors field, which is an array of error objects. Each error object contains details such as the error message, the location of the error in the query, and any extensions provided by the server.

{
  "data": null,
  "errors": [
    {
      "message": "Cannot query field 'nonExistentField' on type 'User'.",
      "locations": [
        {
          "line": 3,
          "column": 5
        }
      ],
      "path": [
        "users",
        0,
        "nonExistentField"
      ]
    }
  ]
}

Why is Error Handling Important?

  • User Experience: Proper error handling prevents the app from crashing and provides informative messages to the user.
  • Debugging: Detailed error messages assist developers in identifying and resolving issues quickly.
  • Resilience: Handling network and server-side errors ensures the app remains functional even under adverse conditions.

Setting Up GraphQL Client in Flutter

First, set up a GraphQL client in your Flutter project. A popular choice is the graphql_flutter package.

Step 1: Add Dependency

Add the graphql_flutter package to your pubspec.yaml file:

dependencies:
  flutter:
    sdk: flutter
  graphql_flutter: ^5.1.0

Run flutter pub get to install the package.

Step 2: Initialize GraphQL Client

Initialize the GraphQL client and wrap your app with GraphQLProvider:

import 'package:flutter/material.dart';
import 'package:graphql_flutter/graphql_flutter.dart';

void main() {
  final HttpLink httpLink = HttpLink(
    'https://your-graphql-endpoint.com/graphql',
  );

  final ValueNotifier client = ValueNotifier(
    GraphQLClient(
      link: httpLink,
      cache: GraphQLCache(store: InMemoryStore()),
    ),
  );

  runApp(
    GraphQLProvider(
      client: client,
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'GraphQL Error Handling',
      home: Scaffold(
        appBar: AppBar(
          title: Text('GraphQL Error Handling'),
        ),
        body: GraphQLQueryExample(),
      ),
    );
  }
}

Handling Errors with Query Widget

The graphql_flutter package provides a Query widget that simplifies fetching data and handling responses. It automatically manages the loading, error, and data states.

import 'package:flutter/material.dart';
import 'package:graphql_flutter/graphql_flutter.dart';

class GraphQLQueryExample extends StatelessWidget {
  final String getUsersQuery = """
    query GetUsers {
      users {
        id
        name
        email
      }
    }
  """;

  @override
  Widget build(BuildContext context) {
    return Query(
      options: QueryOptions(
        document: gql(getUsersQuery),
      ),
      builder: (QueryResult result, {Refetch? refetch, FetchMore? fetchMore}) {
        if (result.hasException) {
          return Center(
            child: Text('Error: ${result.exception.toString()}'),
          );
        }

        if (result.isLoading) {
          return Center(
            child: CircularProgressIndicator(),
          );
        }

        final userList = result.data?['users'] as List;

        return ListView.builder(
          itemCount: userList.length,
          itemBuilder: (context, index) {
            final user = userList[index];
            return ListTile(
              title: Text(user['name']),
              subtitle: Text(user['email']),
            );
          },
        );
      },
    );
  }
}

In this example:

  • Query widget fetches data using the provided GraphQL query.
  • If result.hasException is true, an error message is displayed.
  • If result.isLoading is true, a loading indicator is shown.
  • If data is successfully fetched, it is displayed in a ListView.

Custom Error Handling

For more granular control over error handling, you can directly inspect the result.exception property.

import 'package:flutter/material.dart';
import 'package:graphql_flutter/graphql_flutter.dart';

class CustomGraphQLQueryExample extends StatelessWidget {
  final String getUsersQuery = """
    query GetUsers {
      users {
        id
        name
        email
      }
    }
  """;

  @override
  Widget build(BuildContext context) {
    return Query(
      options: QueryOptions(
        document: gql(getUsersQuery),
      ),
      builder: (QueryResult result, {Refetch? refetch, FetchMore? fetchMore}) {
        if (result.hasException) {
          return Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text('GraphQL Error Occurred!'),
                SizedBox(height: 8),
                ElevatedButton(
                  onPressed: () {
                    print('Error details: ${result.exception!.graphqlErrors}');
                    showDialog(
                      context: context,
                      builder: (BuildContext context) {
                        return AlertDialog(
                          title: Text('Error Details'),
                          content: SingleChildScrollView(
                            child: ListBody(
                              children: result.exception!.graphqlErrors.map((e) => Text(e.message)).toList(),
                            ),
                          ),
                          actions: [
                            TextButton(
                              child: Text('Close'),
                              onPressed: () {
                                Navigator.of(context).pop();
                              },
                            ),
                          ],
                        );
                      },
                    );
                  },
                  child: Text('Show Error Details'),
                ),
              ],
            ),
          );
        }

        if (result.isLoading) {
          return Center(
            child: CircularProgressIndicator(),
          );
        }

        final userList = result.data?['users'] as List;

        return ListView.builder(
          itemCount: userList.length,
          itemBuilder: (context, index) {
            final user = userList[index];
            return ListTile(
              title: Text(user['name']),
              subtitle: Text(user['email']),
            );
          },
        );
      },
    );
  }
}

This example includes a button that displays detailed error messages from result.exception!.graphqlErrors in a dialog. This approach helps provide more specific error information to the user.

Handling Network Errors

Network errors can occur due to connectivity issues. You can handle these by checking the linkException property in the result.exception.

import 'package:flutter/material.dart';
import 'package:graphql_flutter/graphql_flutter.dart';

class NetworkErrorGraphQLQueryExample extends StatelessWidget {
  final String getUsersQuery = """
    query GetUsers {
      users {
        id
        name
        email
      }
    }
  """;

  @override
  Widget build(BuildContext context) {
    return Query(
      options: QueryOptions(
        document: gql(getUsersQuery),
      ),
      builder: (QueryResult result, {Refetch? refetch, FetchMore? fetchMore}) {
        if (result.hasException) {
          if (result.exception!.linkException != null) {
            return Center(
              child: Text('Network Error: ${result.exception!.linkException.toString()}'),
            );
          } else {
            return Center(
              child: Text('GraphQL Error: ${result.exception!.graphqlErrors.toString()}'),
            );
          }
        }

        if (result.isLoading) {
          return Center(
            child: CircularProgressIndicator(),
          );
        }

        final userList = result.data?['users'] as List;

        return ListView.builder(
          itemCount: userList.length,
          itemBuilder: (context, index) {
            final user = userList[index];
            return ListTile(
              title: Text(user['name']),
              subtitle: Text(user['email']),
            );
          },
        );
      },
    );
  }
}

In this code, network-related exceptions are checked, and a specific message is displayed to inform the user about connectivity issues.

Global Error Handling with ErrorLink

For more advanced error handling, you can use ErrorLink from graphql_flutter to handle errors at the link level. This is useful for logging errors or implementing retry mechanisms.

Step 1: Import Necessary Libraries

import 'package:graphql_flutter/graphql_flutter.dart';

Step 2: Implement ErrorLink

import 'package:flutter/material.dart';
import 'package:graphql_flutter/graphql_flutter.dart';

void main() {
  final HttpLink httpLink = HttpLink(
    'https://your-graphql-endpoint.com/graphql',
  );

  final ErrorLink errorLink = ErrorLink(
    onException: (request, response, exception) {
      print('GraphQL Exception: $exception');
      // Handle exception globally, e.g., log to analytics
    },
    onError: (request, response, context) {
      print('GraphQL Error: ${response.errors}');
      // Handle GraphQL errors globally
    },
  );

  final Link link = errorLink.concat(httpLink);

  final ValueNotifier client = ValueNotifier(
    GraphQLClient(
      link: link,
      cache: GraphQLCache(store: InMemoryStore()),
    ),
  );

  runApp(
    GraphQLProvider(
      client: client,
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'GraphQL Error Handling',
      home: Scaffold(
        appBar: AppBar(
          title: Text('GraphQL Error Handling'),
        ),
        body: GraphQLQueryExample(),
      ),
    );
  }
}

Here, ErrorLink intercepts GraphQL exceptions and errors, allowing you to handle them globally. For instance, you can log errors to an analytics service or display a generic error message.

Conclusion

Handling GraphQL errors in Flutter is essential for creating robust and user-friendly applications. By using the Query widget and implementing custom error handling strategies, you can provide detailed feedback to users and simplify the debugging process. Furthermore, leveraging ErrorLink allows you to handle errors globally, ensuring consistent error management across your application.