Di GraphQL, error datang di dalam body response, di array errors, dan sering bersama data yang terisi sebagian. Bayangin kamu memesan dua buku. Satu ada, satu ternyata gak ada di gudang. Kurir tetap mengantar buku yang ada, plus secarik catatan: "buku kedua gak ditemukan, ini alasannya". Paketnya adalah data, catatannya adalah errors.
🧪 Tebakan cepat: Query
{ laskar: book(id: 1) { title } hilang: book(id: 99) { title } }. Buku 99 gak ada. Apakah judul Laskar Pelangi tetap sampai ke client, atau seluruh response gagal? Jawabannya ada di bagian "Isi satu error".
Dua jenis error
Spesifikasi GraphQL membedakan dua jenis:
- Request error: request-nya gak bisa dijalankan sama sekali. Query gagal dibaca, gagal validasi, operation-nya gak jelas, atau variables-nya gak cocok. Response-nya cuma
errors, tanpadata. Biasanya salah di client. - Execution error (dulu disebut field error): satu field gagal waktu dijalankan, misalnya resolver melempar error. Field itu jadi
null, field lain tetap jalan, jadi response-nya berisidatasebagian danerrors. Biasanya masalah di server.
Isi satu error
Ini response Toko Buku untuk query di tebakan:
{
"errors": [
{
"message": "Book not found",
"locations": [{ "line": 5, "column": 3 }],
"path": ["hilang"],
"extensions": { "code": "NOT_FOUND" }
}
],
"data": {
"laskar": { "title": "Laskar Pelangi" },
"hilang": null
}
}
Laskar Pelangi tetap sampai, hilang bernilai null, dan errors menjelaskan kenapa. Itu jawaban tebakannya.
| Key | Isinya |
|---|---|
message |
penjelasan untuk developer. Wajib ada. |
locations |
baris dan kolom di teks query tempat masalahnya, mulai dari 1 |
path |
posisi di response yang jadi null: key-nya (di sini alias hilang), dan nomor urut kalau di dalam list, misalnya ["books", 2, "author"] |
extensions |
map bebas untuk info tambahan dari server, misalnya code |
path itulah yang membedakan null karena error dari null yang memang nilainya. Spesifikasi juga melarang server menambah key lain di samping keempat key ini: info tambahan masuk ke extensions.
extensions.code: kode untuk program
message ditulis untuk manusia dan bisa berubah. Program sebaiknya membaca kode di extensions. Spesifikasi GraphQL gak menentukan isinya. Nama-nama yang paling sering dipakai datang dari Apollo Server:
| Kode | Kapan Apollo Server memakainya |
|---|---|
GRAPHQL_PARSE_FAILED |
query gagal dibaca |
GRAPHQL_VALIDATION_FAILED |
query gak cocok dengan schema |
BAD_USER_INPUT |
variables atau argument gak valid |
OPERATION_RESOLUTION_FAILURE |
operation yang dijalankan gak bisa ditentukan |
INTERNAL_SERVER_ERROR |
error resolver yang gak punya kode sendiri |
Kode lain kamu tentukan sendiri, misalnya NOT_FOUND, atau UNAUTHENTICATED dan FORBIDDEN yang dicontohkan dokumentasi Apollo untuk auth. Di resolver:
import { GraphQLError } from "graphql";
const book = books.find((b) => String(b.id) === args.id);
if (!book) {
throw new GraphQLError("Book not found", { extensions: { code: "NOT_FOUND" } });
}
Di client, cabangkan berdasarkan kode, bukan teks pesan: if (error.extensions?.code === "NOT_FOUND") tampilkanHalaman404().
Menyembunyikan error internal di production
Kalau database penulis mati, resolver Book.author melempar error dari driver database, dan pesannya bocor apa adanya:
{ "message": "connect ECONNREFUSED 10.0.0.5:5432", "path": ["book", "author"] }
Itu alamat server internal, dan pengguna gak bisa berbuat apa-apa dengan info itu. Di production, ganti pesan error yang gak terduga, dan simpan aslinya di log server. Di Apollo Server lewat formatError:
const server = new ApolloServer({
typeDefs,
resolvers,
formatError: (formattedError, error) => {
if (formattedError.extensions?.code === "INTERNAL_SERVER_ERROR") {
console.error(error); // aslinya tetap tercatat di server
return { ...formattedError, message: "Internal server error" };
}
return formattedError;
},
});
Error yang memang untuk client, seperti NOT_FOUND dan BAD_USER_INPUT, dibiarkan. GraphQL Yoga melakukan penyamaran seperti ini secara default.
Error sebagai data: union UpdateStockResult
Stok negatif bukan kerusakan server. Itu kesalahan wajar dari form yang harus ditampilkan ke pengguna. Pola errors as data memasukkannya ke schema. Untuk pelajaran ini, updateStock di Toko Buku mengembalikan union:
type Mutation {
updateStock(id: ID!, stock: Int!): UpdateStockResult
}
union UpdateStockResult = Book | StockError
type StockError {
message: String!
}
mutation {
updateStock(id: 2, stock: -1) {
__typename
... on Book {
title
stock
}
... on StockError {
message
}
}
}
{ "data": { "updateStock": { "__typename": "StockError", "message": "Stock cannot be negative" } } }
Gak ada errors sama sekali. Resolver-nya cukup if (stock < 0) return { message: "Stock cannot be negative" };, dan __resolveType memilih Book atau StockError. Bandingkan dengan versi yang melempar error: updateStock jadi null, dan pesannya ada di errors.
- Sebagai data untuk kesalahan yang diharapkan dan harus ditangani UI: stok negatif, email sudah terdaftar. Kelebihannya, kemungkinan hasilnya tertulis di schema, jadi client dipaksa menanganinya.
- Dilempar ke
errorsuntuk yang gak terduga atau berlaku di mana-mana: database mati, bug, belum login.
Pola userErrors di pelajaran "Mutations" adalah cara lain untuk ide yang sama.
Status HTTP: dua media type
Draft spesifikasi GraphQL over HTTP memakai media type application/graphql-response+json. Dengan itu client tahu body-nya pasti response GraphQL, jadi server boleh memakai status 4xx yang tepat. Dengan application/json yang lama, banyak server, termasuk Apollo Server, menjawab 200 begitu operation-nya dijalankan.
| Kasus | Draft, application/graphql-response+json |
Apollo Server, application/json |
|---|---|---|
Berhasil, tanpa errors |
200 | 200 |
data sebagian plus errors |
294 | 200 |
| Gagal dibaca | 400 | 400 |
| Gagal validasi | 422 | 400 |
| Variables gak cocok | 422 | 400 |
Kode 294 (Partial Success) belum terdaftar resmi, jadi client yang gak mengenalnya memperlakukannya seperti 200. Apa pun media type-nya, client tetap harus membaca errors di body.
⚠️ Jebakan yang sering ketemu
- Cuma mengecek
response.ok. Error resolver sering datang dengan status 200. Bacaerrors. - Logika berdasarkan teks
message. Pesan bisa diubah kapan saja. Pakaiextensions.code. - Pesan internal sampai ke pengguna. Samarkan
INTERNAL_SERVER_ERRORdiformatError, catat aslinya di log. - Kesalahan form dilempar sebagai error biasa. Apollo Server memberinya
INTERNAL_SERVER_ERROR. Beri kodeBAD_USER_INPUT, atau jadikan data lewat union.
TL;DR: Error GraphQL ada di array errors, bersama data yang mungkin sebagian. Request error (gagal dibaca, gagal validasi, variables salah) menghentikan semuanya tanpa data; execution error membuat satu field null dan sisanya tetap terkirim. Tiap error punya message, locations, path, dan extensions, map bebas untuk metadata seperti code. Kode seperti BAD_USER_INPUT dan INTERNAL_SERVER_ERROR adalah konvensi Apollo Server, bukan bagian spesifikasi. Di production, samarkan pesan error internal dan simpan aslinya di log. Kesalahan yang diharapkan bisa jadi data lewat union seperti UpdateStockResult = Book | StockError. Status HTTP: draft GraphQL over HTTP dengan application/graphql-response+json memakai 294, 400, dan 422; dengan application/json, banyak server menjawab 200. Selalu baca body-nya.