Fix Common Errors

Fix "Unexpected End of JSON Input" Error

Debug 'unexpected end of JSON input' errors: identify truncated responses, implement defensive parsing, and monitor JSON failures in production.

"Unexpected end of JSON input" is one of those cryptic errors that stops your app dead without telling you much. Your code calls JSON.parse() or fetches an API, and suddenly a silent failure or a thrown exception derails the flow. Unlike errors with clear stack traces, this one often hides the real culprit—a truncated response, a failed server, or malformed data from a third-party service. If you're seeing this in production, you're losing users and revenue while the root cause remains invisible.

The error itself is deceptively simple: JavaScript's JSON parser hit the end of input while still expecting more data to complete a valid JSON structure. But why that happened—and how to prevent it—requires digging into the network, server state, and your error tracking. This guide walks you through causes, fixes, and how to catch this error in production before it affects your users.

What Causes "Unexpected End of JSON Input"?

The error fires when JSON.parse() receives incomplete or truncated JSON. A few common scenarios:

Network interruption or timeout: Your fetch or HTTP request is cut off mid-response. The server sends {"user": "alice", "ro and then the connection dies. The client receives partial JSON and crashes when trying to parse it.

Server error returning HTML: You expect JSON but the server returns an HTTP 500 with an HTML error page. Calling JSON.parse() on <html><body>500 Internal Server Error</body></html> throws immediately.

Streaming response incomplete: If your code tries to parse a streaming response before all chunks arrive, or if a chunk boundary splits the JSON in an awkward place, you get a partial payload.

Empty or null response: Some APIs return an empty body on certain conditions, or the response is literally null. Parsing an empty string or whitespace-only input fails.

Third-party service returning garbage: An external API hiccup sends corrupted or truncated JSON. Your code has no defense.

The error message itself never tells you what was actually received. That's why production monitoring with breadcrumbs and context is so critical—you need to see the raw response body, status code, and timing to know what went wrong.

How to Reproduce and Debug Locally

In the browser, the error is straightforward:

// This throws: SyntaxError: Unexpected end of JSON input
const incomplete = '{"name": "alice", "age": ';
JSON.parse(incomplete);

In Node.js, the same applies:

const fetch = require('node-fetch');

async function fetchUser(id) {
  const res = await fetch(`https://api.example.com/users/${id}`);
  const text = await res.text();
  // If res.text() is truncated, this line fails
  const data = JSON.parse(text);
  return data;
}

The fix starts with defensive parsing:

async function fetchUser(id) {
  const res = await fetch(`https://api.example.com/users/${id}`);
  
  // Always check the status
  if (!res.ok) {
    throw new Error(`API error: ${res.status} ${res.statusText}`);
  }
  
  const text = await res.text();
  
  // Validate the response is not empty
  if (!text || !text.trim()) {
    throw new Error('Empty response from API');
  }
  
  try {
    const data = JSON.parse(text);
    return data;
  } catch (err) {
    // Log the actual response for debugging
    console.error('JSON parse failed. Received:', text);
    throw new Error(`Failed to parse JSON: ${err.message}`);
  }
}

The key defensive moves:

  • Check HTTP status before parsing.
  • Validate the response is not empty.
  • Wrap JSON.parse() in a try-catch and log the actual input on failure.

This is the bare minimum. In production, you need more context.

Testing for Malformed JSON Responses

Unit tests help catch this early. Here's a simple test suite using Node.js and Jest:

jest.mock('node-fetch');
const fetch = require('node-fetch');
const { fetchUser } = require('./api');

test('handles truncated JSON response', async () => {
  fetch.mockResolvedValueOnce({
    ok: true,
    text: async () => '{"name": "alice", "age": ',
  });
  
  await expect(fetchUser(1)).rejects.toThrow('Failed to parse JSON');
});

test('handles empty response', async () => {
  fetch.mockResolvedValueOnce({
    ok: true,
    text: async () => '',
  });
  
  await expect(fetchUser(1)).rejects.toThrow('Empty response from API');
});

test('handles non-JSON response (HTML error page)', async () => {
  fetch.mockResolvedValueOnce({
    ok: false,
    status: 500,
    statusText: 'Internal Server Error',
  });
  
  await expect(fetchUser(1)).rejects.toThrow('API error: 500');
});

These tests confirm your code doesn't crash silently when the API misbehaves.

Handling the Error Gracefully

You can't always fix the server, but you can degrade gracefully in the client:

async function fetchUserWithFallback(id, fallback = {}) {
  try {
    return await fetchUser(id);
  } catch (err) {
    console.warn('Failed to fetch user, using fallback:', err.message);
    return fallback;
  }
}

// Usage
const user = await fetchUserWithFallback(1, { name: 'Guest', age: null });

Or retry with exponential backoff:

async function fetchUserWithRetry(id, maxRetries = 3) {
  for (let attempt = 1; attempt &lt;= maxRetries; attempt++) {
    try {
      return await fetchUser(id);
    } catch (err) {
      if (attempt === maxRetries) throw err;
      const delay = Math.pow(2, attempt) * 100; // 200ms, 400ms, 800ms
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
}

Retries help when the error is transient (a momentary network glitch or server restart). Fallbacks help when you can still ship a degraded experience.

Catching JSON Errors in Production

Retries and fallbacks reduce the impact, but they don't eliminate the problem—you still need to know when this is happening at scale. This is where monitoring shines.

If you're using a Sentry SDK (or any Sentry-compatible platform like LightTrace), configure it to capture these errors automatically:

import * as Sentry from "@sentry/browser"; // or @sentry/node, @sentry/react, etc.

Sentry.init({
  dsn: "https://<key>@light-trace.robomiri.com/1",
  environment: "production",
  tracesSampleRate: 0.1, // 10% of transactions
});

async function fetchUser(id) {
  const res = await fetch(`https://api.example.com/users/${id}`);
  if (!res.ok) {
    throw new Error(`API error: ${res.status}`);
  }
  const text = await res.text();
  if (!text.trim()) {
    throw new Error('Empty response from API');
  }
  return JSON.parse(text);
}

When JSON.parse() throws or any unhandled error bubbles up, Sentry captures the full stack trace, breadcrumbs (prior network requests, console logs), and context. You see exactly what the code was doing before the failure.

Even if you wrap JSON parsing in a try-catch, errors can still escape as unhandled promise rejections. Make sure your SDK is configured to catch unhandledrejection events.

Adding Custom Context

Add context to every API request so you know what was expected:

async function fetchUser(id) {
  Sentry.captureMessage(`Fetching user ${id}`, 'info');
  
  const res = await fetch(`https://api.example.com/users/${id}`);
  Sentry.addBreadcrumb({
    category: 'api',
    message: `User API response`,
    level: 'info',
    data: {
      status: res.status,
      contentLength: res.headers.get('content-length'),
    },
  });
  
  if (!res.ok) {
    throw new Error(`API error: ${res.status}`);
  }
  
  const text = await res.text();
  if (!text.trim()) {
    throw new Error('Empty response from API');
  }
  
  try {
    return JSON.parse(text);
  } catch (err) {
    Sentry.captureException(err, {
      contexts: {
        response: {
          status: res.status,
          bodyLength: text.length,
          bodyPreview: text.substring(0, 500),
        },
      },
    });
    throw err;
  }
}

Now when the error surfaces in production, you see the actual response body (truncated preview), status code, content length, and exactly which API call failed. That context turns a cryptic error into a solvable problem.

Best Practices

  • Always validate HTTP status before parsing the response body.
  • Log or capture the raw response when parsing fails—the error message alone is useless.
  • Use try-catch around JSON.parse() and include context about what you expected.
  • Implement retries for transient failures; use exponential backoff to avoid hammering a struggling server.
  • Test malformed responses in your test suite, including empty, truncated, and HTML error pages.
  • Monitor in production with a tool that captures breadcrumbs and context, so you see what actually happened when the error fires.

For deeper guidance on debugging production errors systematically, see how to debug production errors and how to read a stack trace. Understanding error context is critical—related errors like uncaught-in-promise can also hide behind silent failures.

Start tracking errors in minutes

If you're seeing "unexpected end of JSON input" in production and want to catch it before your users do, try LightTrace free. Capture full stack traces, breadcrumbs, and response context for every parsing error—no SDK changes needed if you're already using Sentry.

Every JSON error in production should be actionable. With proper error tracking and defensive code, they become fixable problems instead of mysterious black holes.

Fix your next production error faster

Point any Sentry SDK at LightTrace — free up to 5,000 events/month.