Pola Penanganan Error di GraphQL: Membangun API yang Robust dan User-Friendly
1. Pendahuluan
Sebagai developer, kita tahu bahwa error adalah bagian tak terpisahkan dari setiap aplikasi. Namun, cara kita menangani dan mengkomunikasikan error ini kepada klien (frontend, aplikasi mobile, atau layanan lain) sangat krusial untuk pengalaman developer (DX) dan pengalaman pengguna (UX). Di dunia GraphQL, penanganan error memiliki karakteristik unik yang berbeda dari REST API tradisional.
Secara default, GraphQL memiliki mekanisme error handling bawaan, tapi seringkali itu tidak cukup informatif atau fleksibel untuk kebutuhan aplikasi modern yang kompleks. Bayangkan pengguna mencoba login, tapi gagal. Pesan error default yang generik seperti “Internal Server Error” atau “Validation Error” tidak membantu mereka memahami apa yang salah, apalagi membantu developer frontend mendebug masalahnya.
Di artikel ini, kita akan menyelami berbagai pola dan strategi untuk menangani error di GraphQL. Kita akan mulai dari memahami mekanisme bawaan, kemudian menjelajahi bagaimana kita bisa memperkaya informasi error, hingga mengimplementasikan pola “Error as Data” yang type-safe, dan diakhiri dengan praktik terbaik untuk membangun API GraphQL yang robust dan user-friendly.
🎯 Mengapa Penanganan Error di GraphQL Penting?
- Pengalaman Pengguna Lebih Baik: Pesan error yang jelas membimbing pengguna untuk menyelesaikan masalah atau memahami situasinya.
- Debugging Lebih Cepat: Developer frontend mendapatkan konteks yang cukup untuk mengidentifikasi dan memperbaiki bug tanpa harus menebak-nebak atau bolak-balik ke backend.
- Konsistensi API: Klien tahu persis format error yang diharapkan, mengurangi kebingungan dan kode boilerplate.
- Keamanan: Mencegah eksposur detail internal sistem yang sensitif.
Mari kita mulai petualangan kita dalam menguasai penanganan error di GraphQL!
2. Memahami Struktur Error GraphQL Standar
Ketika terjadi error di GraphQL, server akan mengembalikan respons HTTP 200 OK (ini seringkali mengejutkan developer yang terbiasa dengan status HTTP seperti 4xx atau 5xx untuk error di REST) bersama dengan objek JSON yang berisi array errors.
Berikut adalah contoh respons error standar GraphQL:
{
"errors": [
{
"message": "Field 'email' is required.",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"createUser"
]
}
],
"data": null
}
Setiap objek di array errors biasanya memiliki properti berikut:
message: Deskripsi error yang bisa dibaca manusia.locations: Lokasi di query GraphQL di mana error terjadi (baris dan kolom).path: Jalur ke field di query yang menyebabkan error.
📌 Keterbatasan Error Standar:
Meskipun memberikan informasi dasar, error standar ini seringkali kurang spesifik. Misalnya, dari message: "Field 'email' is required.", kita tahu email dibutuhkan, tapi kita tidak tahu kode error spesifik, atau informasi tambahan yang relevan untuk UI (misalnya, apakah ini error validasi, error otorisasi, atau error server internal?).
Ini membuat penanganan error di sisi klien menjadi lebih sulit karena harus melakukan parsing string message atau bergantung pada path dan locations yang tidak selalu konsisten untuk berbagai jenis error.
3. Memperkaya Error dengan extensions
Salah satu cara paling umum dan direkomendasikan untuk menambahkan konteks ke error GraphQL adalah dengan menggunakan properti extensions. Properti ini adalah objek kustom yang bisa Anda isi dengan data tambahan apa pun yang relevan.
💡 Contoh Penggunaan extensions:
Misalnya, kita ingin memberikan kode error spesifik, status HTTP yang lebih bermakna (meskipun respons HTTP utama tetap 200), dan detail validasi.
# Definisi error di server (contoh menggunakan Apollo Server)
type Query {
getUser(id: ID!): User
}
type Mutation {
createUser(input: UserInput!): UserPayload
}
input UserInput {
name: String!
email: String!
password: String!
}
type User {
id: ID!
name: String!
email: String!
}
type UserPayload {
user: User
errors: [UserError]
}
interface UserError {
message: String!
code: String!
}
type ValidationError implements UserError {
message: String!
code: String!
field: String!
value: String
}
type AuthenticationError implements UserError {
message: String!
code: String!
}
Dan implementasi di resolver (pseudo-code):
// Contoh di Node.js dengan Apollo Server
const { GraphQLError } = require('graphql');
const resolvers = {
Mutation: {
createUser: (parent, { input }) => {
// Validasi input
if (!input.email || !input.email.includes('@')) {
throw new GraphQLError('Email tidak valid.', {
extensions: {
code: 'BAD_USER_INPUT',
httpStatus: 400,
field: 'email',
details: 'Format email tidak benar.'
}
});
}
// Logika pembuatan user
// ...
// Jika user sudah ada
if (userExists(input.email)) {
throw new GraphQLError('Email sudah terdaftar.', {
extensions: {
code: 'DUPLICATE_RESOURCE',
httpStatus: 409,
field: 'email'
}
});
}
// Jika ada error internal server
try {
// ... operasi database ...
} catch (error) {
throw new GraphQLError('Terjadi kesalahan internal server.', {
extensions: {
code: 'INTERNAL_SERVER_ERROR',
httpStatus: 500,
traceId: 'abc-123', // Untuk debugging
originalError: error.message // Hati-hati jangan ekspos terlalu banyak
}
});
}
return { /* user data */ };
},
},
};
Dengan pendekatan ini, respons error akan terlihat seperti:
{
"errors": [
{
"message": "Email tidak valid.",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"createUser"
],
"extensions": {
"code": "BAD_USER_INPUT",
"httpStatus": 400,
"field": "email",
"details": "Format email tidak benar."
}
}
],
"data": null
}
✅ Keuntungan extensions:
- Fleksibilitas: Anda bisa menambahkan informasi apa pun yang dibutuhkan.
- Klarifikasi: Memberikan konteks yang lebih kaya daripada sekadar
message. - Standarisasi: Memungkinkan Anda mendefinisikan kode error kustom yang konsisten di seluruh API Anda.
❌ Kekurangan extensions:
- Tidak Type-Safe: Klien harus tahu properti apa yang diharapkan di
extensionssecara implisit. Tidak ada validasi skema GraphQL untuk isiextensions. - Masih di Array
errors: Beberapa developer mungkin lebih suka error sebagai bagian dari data yang dikembalikan.
4. Pola “Error as Data” (Union/Interface Types)
Pola ini melihat error bukan sebagai “pengecualian” yang selalu memicu array errors di respons, melainkan sebagai “hasil alternatif” dari suatu operasi. Dengan kata lain, suatu operasi bisa mengembalikan data yang sukses, atau data yang merepresentasikan error, keduanya didefinisikan secara eksplisit dalam skema GraphQL.
Analogi sederhananya: Ketika Anda meminta minuman di kafe, Anda bisa mendapatkan kopi yang Anda pesan (sukses), atau Anda bisa diberi tahu bahwa kopi habis (error). Keduanya adalah “hasil” dari permintaan Anda.
💡 Implementasi “Error as Data”:
Kita akan menggunakan Union atau Interface types di skema GraphQL.
Pertama, definisikan tipe untuk hasil sukses dan tipe untuk error:
# Skema GraphQL
type Query {
# ...
}
type Mutation {
login(email: String!, password: String!): LoginResult!
}
type User {
id: ID!
email: String!
name: String
}
# Definisi Union Type untuk hasil login
union LoginResult = AuthSuccess | AuthError
type AuthSuccess {
token: String!
user: User!
}
interface AuthError {
code: String!
message: String!
}
type InvalidCredentialsError implements AuthError {
code: String!
message: String!
}
type UserNotFoundError implements AuthError {
code: String!
message: String!
}
# ... tipe error lainnya
Kemudian, di resolver Anda, alih-alih melempar error, Anda akan mengembalikan objek error yang sesuai dengan skema:
// Contoh resolver untuk login
const resolvers = {
Mutation: {
login: async (parent, { email, password }) => {
const user = await findUserByEmail(email);
if (!user) {
return {
__typename: 'UserNotFoundError',
code: 'USER_NOT_FOUND',
message: 'Pengguna dengan email tersebut tidak ditemukan.'
};
}
if (!await verifyPassword(user.id, password)) {
return {
__typename: 'InvalidCredentialsError',
code: 'INVALID_CREDENTIALS',
message: 'Kombinasi email dan kata sandi salah.'
};
}
const token = generateAuthToken(user);
return {
__typename: 'AuthSuccess',
token,
user
};
}
},
LoginResult: {
__resolveType(obj, context, info) {
if (obj.token && obj.user) {
return 'AuthSuccess';
}
if (obj.code && obj.message) {
// Ini adalah cara sederhana, mungkin perlu properti yang lebih spesifik
// untuk membedakan antara InvalidCredentialsError dan UserNotFoundError
if (obj.code === 'USER_NOT_FOUND') {
return 'UserNotFoundError';
}
if (obj.code === 'INVALID_CREDENTIALS') {
return 'InvalidCredentialsError';
}
}
return null;
},
},
};
Klien kemudian akan query seperti ini:
mutation UserLogin($email: String!, $password: String!) {
login(email: $email, password: $password) {
... on AuthSuccess {
token
user {
id
name
email
}
}
... on InvalidCredentialsError {
code
message
}
... on UserNotFoundError {
code
message
}
# ... handle other error types
}
}
Respons sukses:
{
"data": {
"login": {
"token": "...",
"user": {
"id": "123",
"name": "John Doe",
"email": "john@example.com"
}
}
}
}
Respons error (sebagai data):
{
"data": {
"login": {
"code": "INVALID_CREDENTIALS",
"message": "Kombinasi email dan kata sandi salah."
}
}
}
✅ Keuntungan “Error as Data”:
- Type-Safe: Klien tahu persis tipe error yang mungkin terjadi dan dapat menanganinya dengan pasti (misalnya, menggunakan
switchatauif/elseberdasarkan__typename). - Klarifikasi Skema: Skema GraphQL secara eksplisit mendokumentasikan semua kemungkinan hasil dari suatu operasi, termasuk error.
- Tidak Ada Array
errors: Respons HTTP selalu 200 OK dan tidak ada arrayerrorsjika error adalah bagian dari data yang diharapkan. Ini bisa menyederhanakan logika klien.
❌ Kekurangan “Error as Data”:
- Verbose: Skema bisa menjadi sangat panjang jika setiap operasi memiliki banyak kemungkinan error.
- Kurang Cocok untuk Error Tak Terduga: Lebih cocok untuk “business errors” yang memang merupakan bagian dari domain, bukan untuk “system errors” (misalnya, database down). Untuk system errors,
extensionsatau penanganan global mungkin lebih tepat. - Klien Harus Query Semua Kemungkinan: Klien harus secara eksplisit meminta semua fragmen error yang mungkin.
5. Strategi Penanganan Error Global (Middleware/Interceptor)
Selain penanganan error di level resolver, penting juga untuk memiliki mekanisme penanganan error global. Ini berguna untuk:
- Catch-all Unhandled Exceptions: Menangkap error yang tidak secara eksplisit ditangani di resolver.
- Logging: Mencatat semua error ke sistem monitoring (Sentry, New Relic, etc.).
- Masking Sensitif Data: Memastikan detail stack trace atau data sensitif tidak terekspos ke klien.
- Transformasi Error: Mengubah format error tak terduga