Apa yang Sebenarnya Dikeluhkan Error Ini

Ketika kamu sedang mengembangkan aplikasi web dan tiba-tiba layar konsol browser menampilkan pesan yang mirip seperti berikut:

SyntaxError: JSON.parse: unexpected character at line 1 column 1 of the JSON data

atau dalam variasi Chrome:

Uncaught SyntaxError: Unexpected token < in JSON at position 0

maka yang sedang terjadi adalah JavaScript mencoba mengubah sebuah string menjadi objek JSON menggunakan JSON.parse(), tetapi karakter pertama dari string tersebut bukan merupakan karakter yang valid dalam format JSON. Angka "line 1 column 1" sangat spesifik dan memberitahu kita bahwa masalah sudah ada di karakter pertama — bukan di tengah-tengah struktur JSON yang rumit. Artinya, string yang diberikan ke JSON.parse() sama sekali bukan JSON sejak awal.

Dalam bahasa Indonesia sederhana, error ini berkata: "Hei, kamu menyuruhku membaca JSON, tapi teks yang kamu berikan bukan JSON sama sekali sejak huruf pertama." Ini bukan masalah koma yang kurang atau tanda kutip yang Salah di baris ketiga puluh; ini adalah masalah fundamental di mana data yang diterima sudah salah jenis sejak awal.

Bayangkan sebuah skenario nyata: kamu punya fungsi frontend seperti berikut yang menangkap respons dari API:

fetch('/api/users')
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error('Error:', error));

Metode response.json() di balik layar sebenarnya memanggil JSON.parse() terhadap body respons. Jika server mengirimkan HTML (misalnya halaman login atau halaman error 500) alih-alih JSON, maka JSON.parse() akan menemukan karakter < dari tag <html> di posisi pertama dan langsung melempar error ini. Itulah mengapa error ini hampir selalu muncul ketika ada ketidakcocokan antara apa yang dikirim server dan apa yang diharapkan client.

Kenapa bisa muncul? Secara teknis, JSON yang valid harus dimulai dengan salah satu dari karakter berikut: tanda kurung kurawal { untuk objek, tanda kurung siku [ untuk array, tanda kutip " untuk string, angka, true, false, atau null. Ketika karakter pertama adalah <, S, D, atau karakter lain yang bukan bagian dari spesifikasi JSON, parser langsung menolak dan melempar SyntaxError. Browser tidak punya toleransi di sini — spesifikasi ECMAScript untuk JSON.parse() sangat ketat dan tidak ada opsi untuk "memaksa" parsing.

Akar Masalah yang Paling Sering

Berdasarkan pengalaman komunitas developer dan frekuensi pertanyaan di Stack Overflow, berikut adalah akar masalah sumber masalah tersering untuk error ini, diurutkan dari yang paling umum terjadi:

1. Server mengembalikan HTML alih-alih JSON. Ini adalah penyebab nomor satu dan paling dominan. Sering kali terjadi ketika endpoint API yang kamu panggil mengembalikan halaman error (seperti 404 atau 500), halaman login redirect, atau halaman default Apache/Nginx. Server mengirimkan Content-Type text/html, tetapi kode frontend tetap mencoba mem-parse-nya sebagai JSON. Ini sangat umum di environment development di mana server mungkin belum dikonfigurasi dengan benar atau route belum didaftarkan.

2. Response yang kosong atau undefined. Kadang server mengembalikan body kosong (status 204 No Content) atau bahkan tidak mengirim body sama sekali, sehingga JSON.parse() menerima string kosong "". String kosong bukan JSON yang valid, dan parser akan mengeluh di karakter pertama karena tidak menemukan apa pun.

3. BOM (Byte Order Mark) tersembunyi di awal file JSON. File JSON yang disimpan dengan encoding UTF-8 dengan BOM akan memiliki byte EF BB BF di awal yang, ketika dibaca sebagai teks, muncul sebagai karakter tak terlihat sebelum tanda {. Parser JSON tidak mengenali karakter ini dan langsung gagal. Ini sering terjadi pada file yang dibuat di editor Windows atau disimpan melalui script PHP dengan encoding tertentu.

4. PHP atau backend menyisipkan notice/warning sebelum JSON. Ketika backend PHP menghasilkan JSON tapi ada pesan Notice: Undefined variable atau Warning: yang ter-echo sebelum json_encode(), maka output yang diterima client akan dimulai dengan teks error PHP tersebut, bukan dengan { atau [. Karakter pertama menjadi N atau W dan parsing gagal.

5. CORS yang memblokir dan browser menerima respons error. Ketika permintaan cross-origin ditolak oleh CORS policy, browser kadang tetap menjalankan callback tapi dengan respons yang tidak bisa dibaca, menghasilkan string kosong atau teks error CORS yang di-parse sebagai JSON.

Cara memastikan penyebab mana yang sedang kamu alami adalah dengan membuka DevTools browser (F12), pergi ke tab Network, jalankan ulang permintaan yang gagal, lalu klik request yang bersangkutan dan lihat tab Response. Di sana kamu akan melihat persis apa yang dikirim server. Jika kamu melihat HTML, itu penyebab nomor 1. Jika kosong, penyebab nomor 2. Jika ada karakater aneh sebelum JSON, kemungkinan penyebab nomor 3 atau 4. Kamu juga bisa menggunakan perintah curl di terminal:

curl -i https://api.contoh.com/data

Output akan menampilkan header dan body respons sehingga kamu bisa melihat apakah Content-Type benar dan apakah body dimulai dengan JSON yang valid atau ada teks lain sebelumnya.

Solusi Step-by-Step

Langkah 1: Inspeksi Respons Aktual dari Server

Langkah pertama dan paling krusial adalah melihat dengan mata kepala sendiri apa yang sebenarnya dikirim server. Jangan menebak — periksa.

curl -v https://api.contoh.com/users

Output yang kamu harapkan adalah sesuatu yang dimulai dengan header seperti:

< HTTP/1.1 200 OK
< Content-Type: application/json

Diikuti oleh body yang dimulai dengan { atau [. Jika output dimulai dengan <!DOCTYPE html> atau teks lain, kamu sudah mengidentifikasi masalahnya. Verifikasi berhasil ketika kamu melihat body respons adalah JSON yang valid dan Content-Type header bernilai application/json.

Langkah 2: Perbaiki Endpoint yang Mengembalikan HTML

Jika server mengembalikan HTML, aku masalahnya di sisi backend. Misalnya, di Express.js, pastikan route API terdaftar sebelum route static files:

// SALAH - static files menangkap semua request duluan
app.use(express.static('public'));
app.get('/api/users', (req, res) => {
  res.json([{ id: 1, name: 'Andi' }]);
});

// BENAR - API route didaftarkan duluan
app.get('/api/users', (req, res) => {
  res.json([{ id: 1, name: 'Andi' }]);
});
app.use(express.static('public'));

Di PHP, pastikan tidak ada output sebelum json_encode():

<?php
// SALAH - ada spasi/karakter sebelum <?php atau ada echo sebelumnya
  header('Content-Type: application/json');
  echo json_encode(['status' => 'ok']);
<?php
// BENAR - tidak ada output sebelum header dan json_encode
header('Content-Type: application/json');
echo json_encode(['status' => 'ok']);

Verifikasi: Setelah perbaikan, jalankan curl -i lagi dan pastikan Content-Type adalah application/json serta body dimulai dengan { atau [.

Langkah 3: Tangani Respons Kosong atau Non-JSON di Frontend

Sebagai fix langsung sekaligus defensif, tambahkan pengecekan di frontend sebelum mem-parsing JSON:

async function fetchUsers() {
  const response = await fetch('/api/users');
  
  // Cek Content-Type header
  const contentType = response.headers.get('content-type');
  if (!contentType || !contentType.includes('application/json')) {
    throw new TypeError('Server tidak mengembalikan JSON');
  }
  
  // Cek status code
  if (!response.ok) {
    const text = await response.text();
    throw new Error(`HTTP ${response.status}: ${text}`);
  }
  
  const data = await response.json();
  return data;
}

Dengan cara ini, ketika server salah mengirim HTML, kamu mendapat error message yang jelas bukan "unexpected character" yang membingungkan. Verifikasi: Coba arahkan fetch ke URL yang salah (yang mengembalikan HTML) dan pastikan error yang muncul adalah pesan custom kamu, bukan SyntaxError dari JSON.parse.

Langkah 4: Hapus BOM dari File JSON atau Output Backend

Jika masalahnya adalah BOM tersembunyi, conver file ke UTF-8 tanpa BOM:

# Linux/macOS - menggunakan vim
vim -c 'set nobomb' -c 'x' data.json

# Atau menggunakan sed
sed -i '1s/^\xEF\xBB\xBF//' data.json

# Windows PowerShell
$content = [System.IO.File]::ReadAllText('data.json')
$content = $content.TrimStart([char]0xFEFF)
[System.IO.File]::WriteAllText('data.json', $content, (New-Object System.Text.UTF8Encoding $false))

Di PHP, pastikan output tidak memiliki BOM dengan menggunakan encoding yang benar:

<?php
header('Content-Type: application/json; charset=utf-8');
// Gunakan UTF-8 tanpa BOM
echo json_encode($data, JSON_UNESCAPED_UNICODE);

Verifikasi: Gunakan hexdump atau xxd untuk memastikan byte pertama adalah { (0x7B) bukan BOM (0xEF 0xBB 0xBF):

xxd data.json | head -1
# Keluaran yang tampak: 00000000: 7b22 ... (7b adalah hex untuk {)
# BUKAN: 00000000: efbb bf7b ... (efbbbf adalah BOM)

Langkah 5: Fix Permanen — Konfigurasi Server yang Benar

Untuk fix permanen, konfigurasi server sehingga endpoint API selalu mengembalikan Content-Type yang benar dan tidak pernah mengembalikan HTML pada route API. Di Nginx:

location /api/ {
    # Pastikan header Content-Type selalu JSON untuk API
    add_header Content-Type application/json;
    
    # Proxy ke backend
    proxy_pass http://backend;
    proxy_intercept_errors on;
    
    # Custom error page dalam format JSON
    error_page 404 /api/404.json;
    error_page 500 /api/500.json;
}

Di Apache, gunakan .htaccess:

<IfModule mod_rewrite.c>
    RewriteEngine On
    
    # Force JSON content type untuk API routes
    <LocationMatch "^/api/">
        Header always set Content-Type "application/json"
    </LocationMatch>
</IfModule>

Verifikasi: Restart server, lakukan curl -I https://domain.com/api/endpoint dan pastikan response header selalu menampilkan Content-Type: application/json.

Mencegah Error Ini Kembali

Mencegah error ini kembali membutuhkan pendekatan sistematis yang mencakup konfigurasi server, penulisan kode defensif, dan testing yang tepat. Pendekatan reaktif akan selalu mengejar masalah; pendekatan preventif memastikan masalah tidak muncul lagi.

Setelan agar tidak terulang dimulai dari level server. Pastikan setiap endpoint API secara eksplisit mengatur Content-Type header. Di Express.js, gunakan middleware global yang memastikan semua respons API berformat JSON:

// Middleware yang memaksa semua /api/* route mengembalikan JSON
app.use('/api', (req, res, next) => {
  res.setHeader('Content-Type', 'application/json');
  
  // Override res.send untuk /api routes agar selalu JSON
  const originalSend = res.send;
  res.send = function (data) {
    if (typeof data === 'object') {
      originalSend.call(this, JSON.stringify(data));
    } else if (typeof data === 'string' && data.charAt(0) !== '{' && data.charAt(0) !== '[') {
      // Jika string bukan JSON, bungkus dalam objek error
      originalSend.call(this, JSON.stringify({ error: data }));
    } else {
      originalSend.call(this, data);
    }
  };
  
  next();
});

Konfigurasi serupa di level reverse proxy juga penting, terutama untuk menangani error pages yang secara default berformat HTML:

# Konfigurasi Nginx yang komprehensif untuk API
server {
    listen 443 ssl;
    server_name api.contoh.com;
    
    # Default error responses dalam JSON
    default_type application/json;
    
    error_page 404 = @404_json;
    error_page 500 502 503 504 = @error_json;
    
    location @404_json {
        return 404 '{"error":"Not Found","code":404}';
    }
    
    location @error_json {
        return 500 '{"error":"Internal Server Error","code":500}';
    }
    
    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
    }
}

Kebiasaan yang baik yang harus diadopsi tim meliputi beberapa hal penting. Pertama, selalu gunakan response.json() pada fetch API dengan pengecekan Content-Type terlebih dahulu — jangan langsung memanggil response.json() tanpa verifikasi. Kedua, implementasikan interceptor di HTTP client kamu. Axios misalnya, memiliki mekanisme interceptor yang sangat berguna:

// Axios interceptor untuk menangani respons non-JSON
axios.interceptors.response.use(
  (response) => {
    const contentType = response.headers['content-type'];
    if (contentType && !contentType.includes('application/json')) {
      console.warn(`Expected JSON but received ${contentType}`);
      return Promise.reject(new Error(`Invalid content type: ${contentType}`));
    }
    return response;
  },
  (error) => {
    if (error.response) {
      // Server merespon tapi bukan 2xx
      const data = error.response.data;
      if (typeof data === 'string' && data.charAt(0) === '<') {
        // Respons adalah HTML, bukan JSON
        return Promise.reject(new Error('Server returned HTML instead of JSON'));
      }
    }
    return Promise.reject(error);
  }
);

Ketiga, tambahkan test pada CI/CD pipeline yang secara otomatis memvalidasi bahwa semua endpoint API mengembalikan Content-Type application/json dan body yang bisa di-parse sebagai JSON:

// Contoh test menggunakan Jest + supertest
describe('API Content-Type Validation', () => {
  const apiRoutes = [
    '/api/users',
    '/api/products',
    '/api/orders'
  ];
  
  apiRoutes.forEach(route => {
    test(`${route} returns JSON content type`, async () => {
      const response = await request(app).get(route);
      expect(response.headers['content-type']).toMatch(/application\/json/);
      expect(() => JSON.parse(JSON.stringify(response.body))).not.toThrow();
    });
  });
});

Perbedaan berdasarkan OS perlu diperhatikan karena masalah BOM dan line ending berbeda di setiap sistem operasi. Di Windows, beberapa editor seperti Notepad secara default menyimpan file UTF-8 dengan BOM, sementara di Linux dan macOS, editor default biasanya menyimpan tanpa BOM. Untuk menstandarkan ini di tim yang menggunakan OS berbeda, konfigurasikan editor settings secara konsisten. Di VS Code, tambahkan ke settings.json:

{
  "files.encoding": "utf8",
  "files.eol": "\n",
  "files.insertFinalNewline": true,
  "files.trimTrailingWhitespace": true
}

Di proyek yang menggunakan Git, aktifkan konfigurasi berikut untuk mencegah masalah line ending lintas OS:

# Konfigurasi Git untuk menangani line ending
git config --global core.autocrlf input   # Linux/macOS
git config --global core.autocrlf true    # Windows

# Atau buat .gitattributes di root project
echo "* text=auto eol=lf" > .gitattributes

Untuk project PHP yang di-deploy di server Linux tapi dikembangkan di Windows, pastikan tidak ada BOM di file PHP:

# Script untuk mendeteksi file PHP dengan BOM di seluruh project
find . -name "*.php" -exec grep -l $'\xEF\xBB\xBF' {} \;

# Script untuk menghapus BOM semua file PHP sekaligus
find . -name "*.php" -exec sed -i '1s/^\xEF\xBB\xBF//' {} \;

Troubleshooting Lanjutan

Jika perbaikan di atas masih belum berhasil tapi error tetap muncul, ada beberapa skenario lanjutan yang perlu diperiksa. Kadang masalahnya tidak sesederhana yang terlihat dan membutuhkan pendekatan lebih mendalam.

Pertama, periksa apakah ada service worker atau cache yang menginterferensi. Service worker bisa meng-cache respons lama dan mengembalikannya saat server sudah diperbaiki. Buka DevTools > Application > Service Workers dan unregister semua service worker, lalu clear cache di Application > Cache Storage. Juga bersihkan browser cache dengan hard refresh (Ctrl+Shift+R di Windows/Linux atau Cmd+Shift+R di Mac). Kadang proxy cache seperti Cloudflare atau Varnish juga menyimpan respons HTML yang salah, jadi coba bypass cache:

# Bypass Cloudflare cache dengan curl -H "Cache-Control: no-cache" -H "Pragma: no-cache" https://api.contoh.com/data

Kedua, jika menggunakan framework yang memiliki mekanisme render server-side (seperti Next.js, Nuxt, atau SvelteKit), periksa apakah ada hydration mismatch di mana server mengirim HTML dan client mengharapkan JSON pada endpoint yang sama. Ini sering terjadi saat API routes dan page routes bertabrakan karena konfigurasi routing yang ambigu:

// next.config.js - pastikan API routes tidak bertabrakan dengan page routes
module.exports = {
  async rewrites() {
    return [
      {
        source: '/api/:path*',
        destination: '/server/api/:path*'  // Rutekan ke handler yang benar
      }
    ];
  }
};

Ketiga, cara membaca log dengan tepat. Jangan hanya melihat error di browser console — itu adalah akibat, bukan sebab. Baca log server untuk menemukan akar masalahnya. Di Node.js, tambahkan logging middleware yang mencatat setiap request dan respons:

// Express logging middleware
app.use((req, res, next) => {
  const originalJson = res.json;
  res.json = function (data) {
    console.log(`[${new Date().toISOString()}] ${req.method} ${req.url} -> JSON response`);
    originalJson.call(this, data);
  };
  
  const originalSend = res.send;
  res.send = function (data) {
    const snippet = typeof data === 'string' ? data.substring(0, 100) : 'non-string';
    console.log(`[${new Date().toISOString()}] ${req.method} ${req.url} -> Response starts with: "${snippet}"`);
    originalSend.call(this, data);
  };
  
  next();
});

Di PHP, aktifkan error logging ke file terpisah sehingga notice dan warning tidak tercampur dengan output JSON:

<?php
// Di awal file atau bootstrap
ini_set('display_errors', '0');
ini_set('log_errors', '1');
ini_set('error_log', '/var/log/php/error.log');
error_reporting(E_ALL);
# Pantau log PHP secara real-time
tail -f /var/log/php/error.log

Di Nginx, periksa access log dan error log:

# Monitor kedua log secara bersamaan
tail -f /var/log/nginx/access.log /var/log/nginx/error.log

Keempat, jika menggunakan web server dengan fitur auto-index (seperti Nginx autoindex on atau Apache mod_autoindex), ketika file tidak ditemukan, server bisa mengembalikan direktori listing dalam format HTML. Pastikan autoindex dimatikan untuk API routes:

location /api/ {
    autoindex off;
    # ... konfigurasi lainnya
}

Kelima, periksa apakah ada redirect chain yang menyebabkan permintaan API diarahkan ke halaman login. Ini sangat umum di aplikasi yang menggunakan autentikasi berbasis cookie. Ketika session expired, middleware autentikasi me-redirect ke /login, dan fetch API mendapatkan HTML halaman login alih-alih JSON data. Solusinya adalah memastikan API routes mengembalikan status 401 dalam format JSON, bukan redirect:

// Express auth middleware - JANGAN redirect untuk API
function authMiddleware(req, res, next) {
  if (!req.session.userId) {
    // Cek apakah ini API request
    if (req.path.startsWith('/api/')) {
      return res.status(401).json({ error: 'Unauthorized', code: 401 });
    }
    return res.redirect('/login');  // Hanya redirect untuk non-API
  }
  next();
}

Dokumentasi dan referensi jika kamu ingin memahami lebih lanjut meliputi: spesifikasi JSON di ECMA-404 yang menjelaskan secara detail format JSON yang valid; dokumentasi MDN Web Docs tentang JSON.parse() yang mencakup behavior dan edge cases; dokumentasi Fetch API di MDN tentang Response.json() yang menjelaskan bahwa metode ini melempar TypeError jika respons bukan JSON; serta RFC 7231 Section 3.1.1.5 tentang Content-Type header yang menjelaskan kebutuhan penggunaan header ini secara konsisten. Untuk diskusi komunitas, Stack Overflow tag json-parse-error memiliki ribuan kasus dengan solusi yang sudah diverifikasi komunitas.