Error Handling di GraphQL — GraphQL

Error Handling di GraphQL GraphQL selalu return HTTP 200. Errors ada di response body field errors. Ini berbeda dari REST yang pakai status codes. Default…

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:

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.

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

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.

Yang akan kamu pelajari