JSON Parse Error Explained: Reading SyntaxError Messages and Finding the Cause

Quick answer

💡A JSON parse error is a SyntaxError thrown when the input string violates JSON grammar. The error message includes the unexpected character and its position as a byte offset from the start of the string. Use that offset to locate the exact character causing the failure, then check for trailing commas, single quotes, unquoted keys, JavaScript comments, or a BOM character at position zero.

Error symptoms

  • SyntaxError: Unexpected token X in JSON at position N
  • SyntaxError: JSON.parse: unexpected character at line L column C
  • json.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
  • parse error (Invalid numeric literal) from jq command
  • JSON.parse returns nothing and the try-catch block fires
  • Node.js crashes with uncaught SyntaxError on server startup when reading config file

Common causes

  • Trailing comma after the last element in an array or object
  • String values delimited with single quotes instead of double quotes
  • JavaScript-style comments (// or /* */) included in the JSON source
  • A BOM character (U+FEFF) at byte position 0, written by some text editors
  • Unquoted property keys that are valid JavaScript identifiers but not valid JSON
  • Passing undefined or a non-string value that gets coerced to the string 'undefined'

When it happens

  • When reading a JSON configuration file edited manually in a text editor that inserts comments
  • When an API endpoint returns an HTML error page instead of JSON and the client calls JSON.parse on it
  • When constructing a JSON string by hand using string concatenation and introducing a formatting mistake
  • When a file is saved with BOM encoding by a Windows text editor and then read in a Unix environment
  • When deserializing a message queue payload that was corrupted or truncated in transit

Examples and fixes

The default SyntaxError from JSON.parse provides a position offset but no surrounding context. A wrapper function extracts the relevant portion of the input string to make the error immediately actionable.

Wrapping JSON.parse to produce descriptive error context

❌ Wrong

async function loadUserProfile(userId) {
  const response = await fetch(`/api/users/${userId}`);
  const rawBody = await response.text();
  const userData = JSON.parse(rawBody);
  // If rawBody is malformed, this throws:
  // SyntaxError: Unexpected token < in JSON at position 0
  // No context about what the raw body actually was
  return userData;
}

// Caller sees only an unhandled rejection with no actionable info
loadUserProfile(42).catch(console.error);

✅ Fixed

function parseJsonSafely(rawInput, label = 'input') {
  try {
    return { data: JSON.parse(rawInput), error: null };
  } catch (err) {
    const match = err.message.match(/position (\d+)/);
    const position = match ? parseInt(match[1], 10) : 0;
    const snippet = rawInput.slice(
      Math.max(0, position - 30),
      Math.min(rawInput.length, position + 30)
    );
    const contextMsg = `JSON parse failed in ${label} at position ${position}. ` +
      `Context around error: ...${snippet}...`;
    return { data: null, error: new Error(contextMsg) };
  }
}

async function loadUserProfile(userId) {
  const response = await fetch(`/api/users/${userId}`);
  const rawBody = await response.text();
  const { data, error } = parseJsonSafely(rawBody, `user ${userId} profile`);
  if (error) {
    console.error(error.message);
    throw error;
  }
  return data;
}

The default SyntaxError message tells you there is a problem and where in the string it occurred, but not what the surrounding content looks like. When the input is a long API response body, a position offset of 0 or 2 with no context makes diagnosis nearly impossible. The wrapper function extracts a 60-character window centered on the error position, which usually shows enough of the bad content to identify the problem immediately. Returning an object with data and error rather than throwing keeps the caller in control of error handling and avoids uncaught promise rejections.

A UTF-8 BOM written by certain text editors causes JSON.parse to throw at position 0 with an unexpected token error, even though the rest of the JSON is perfectly valid.

Handling a BOM character at position 0

❌ Wrong

const fs = require('fs');

// config.json was saved with BOM by Notepad on Windows
// File bytes start with EF BB BF (UTF-8 BOM) followed by valid JSON
const rawConfig = fs.readFileSync('./config.json', 'utf8');
console.log(rawConfig.charCodeAt(0)); // 65279 (U+FEFF)

const configData = JSON.parse(rawConfig);
// SyntaxError: Unexpected token \uFEFF in JSON at position 0

console.log(configData.apiBaseUrl); // never reached

✅ Fixed

const fs = require('fs');

function stripBom(str) {
  return str.charCodeAt(0) === 0xFEFF ? str.slice(1) : str;
}

const rawConfig = fs.readFileSync('./config.json', 'utf8');
const cleanConfig = stripBom(rawConfig);

console.log(cleanConfig.charCodeAt(0)); // 123 - opening brace of the JSON

const configData = JSON.parse(cleanConfig);
console.log(configData.apiBaseUrl); // 'https://api.example.com'

// Alternative: use the strip-bom npm package
// import stripBom from 'strip-bom';
// const configData = JSON.parse(stripBom(rawContent));

The UTF-8 BOM is a zero-width no-break space character (U+FEFF) that some text editors prepend to UTF-8 files as an encoding signature. It is invisible in most editors, so the file appears to contain valid JSON when opened. JSON.parse encounters this character before the opening brace and throws immediately because U+FEFF is not a valid JSON whitespace character. The fix is a one-line strip that checks for the BOM by its code point and slices it off when present. This check is cheap and harmless when the BOM is not present.

How JSON parser state machines report errors

JSON parsers are implemented as recursive descent parsers that follow the grammar defined in RFC 8259. The parser maintains a state machine that tracks what character it expects to see next based on what it has already consumed. When the next character in the input does not match any valid option in the current state, the parser halts and reports a SyntaxError.

The error is always thrown at the position of the first unexpected character, not at the position of the root cause. This distinction matters for debugging. A trailing comma after the last element of an array is positioned before the closing bracket. The parser is in a state where it expects either another element or the closing bracket, and instead encounters a comma. The parser reports the position of the comma as the error location, but the real issue is that trailing commas are not permitted in JSON at all.

Common grammar violations include using single quotes instead of double quotes for strings, which the parser encounters as an unexpected character in the value position; including JavaScript-style comments, which the parser encounters as an unexpected forward slash; using unquoted keys in objects, which the parser encounters as an unexpected non-quote character where it expects a string key; and the undefined literal, which the parser encounters as the letter u where it expects a JSON value.

The JSON grammar does permit a small number of whitespace characters between tokens: space, horizontal tab, line feed, and carriage return. These are the only characters that the parser skips over without error. Any other character outside of a string value, including Unicode control characters, the BOM character, or a stray backslash, will cause the parser to throw. The strictness is intentional: JSON is designed to be a minimal, unambiguous data interchange format, and the parser enforces that minimalism without exception.

Decoding position offset into line and column

The position number in a JavaScript JSON parse error is a zero-based character offset from the start of the input string. For a single-line JSON string, this offset is directly the character index you need to inspect. You can extract the relevant character with rawInput[position] or rawInput.charAt(position), and the surrounding context with rawInput.slice(position - 30, position + 30).

For multi-line JSON, a formatted JSON document read from a file, the offset needs to be converted into a line and column number before it is useful. You can compute this by counting newline characters in the substring before the offset: the number of newlines plus one gives the line number, and the distance from the last newline to the offset gives the column. A utility function to perform this computation is worth having in your debugging toolkit for any project that reads JSON from files.

For example, if the error position is 245 and the input contains three newline characters before position 245, the error is on line 4. To find the column, find the index of the last newline before position 245 and subtract it from 245. This computation is also what Python's json.JSONDecodeError does natively: its lineno and colno attributes are pre-computed for you, which is one reason Python's error messages are often more immediately useful for multi-line inputs.

For very large JSON files, megabytes of minified data, the position offset can run into the tens of thousands. In these cases, searching the raw string for the position is impractical manually. A better approach is to run the file through a command-line tool like jq or jsonlint, both of which report errors in line-and-column format. For Node.js scripts, piping the content through python -m json.tool also gives line-and-column output immediately.

When the error position is 0, the problem is almost always one of three things: the input is empty, the input starts with a BOM character, or the input is not JSON at all — typically an HTML error page or a plain text message from a server.

Wrapping JSON.parse with descriptive errors

The most impactful single improvement you can make to any codebase that uses JSON.parse is to wrap every call in a try-catch that logs the raw input alongside the error. Without the raw input, a JSON parse error logged in production is nearly impossible to reproduce: you do not know what the malformed string was, where it came from, or what part of it triggered the error.

The wrapper function pattern works as follows: accept the raw string and an optional label identifying the source, attempt to parse inside a try block, and in the catch block extract the position from the error message using a regular expression. Then slice a window of characters around that position, typically 30 characters before and 30 after, and construct a new error message that includes the label, position, and context window. Log this message and re-throw a new Error with the enriched message, or return a result object with data and error fields.

For server-side Node.js applications, integrate this wrapper into the middleware layer that parses request bodies. Most web frameworks provide a body parser that can be configured to use a custom JSON reviver or error handler. If you are using Express with express.json(), add an error-handling middleware that catches JSON parse errors specifically — they are SyntaxError instances with a status property of 400 set by the middleware — and logs the raw body from req.rawBody before sending a structured error response.

For client-side applications that call fetch and then parse the response body, always call response.text() first and then parse the resulting string rather than calling response.json() directly. The response.json() convenience method does not let you inspect the raw body if parsing fails. Calling response.text() first preserves the raw content so you can log it in your catch block and also inspect the Content-Type header to determine whether the server actually sent JSON at all.

Chrome, Firefox, and Python error formats

Different JavaScript engines and parsers report JSON parse errors with different message formats, which matters when you are writing code that parses the error message to extract position information. V8, which powers both Node.js and Chrome, uses the format: SyntaxError: Unexpected token X in JSON at position N. SpiderMonkey, which powers Firefox, uses a more detailed format: SyntaxError: JSON.parse: unexpected character at line L column C of the JSON data. The Firefox format gives you line and column directly, which is more useful for multi-line JSON. The Chrome and Node.js format gives a raw position offset that requires conversion.

In newer versions of Node.js (v20 and later), the error message format has been updated to match more modern V8 behavior and may differ slightly from older versions. Any code that parses the error message with a regular expression should be tested against the specific Node.js version in use and ideally written to handle both message formats gracefully by trying multiple patterns.

Python's json module raises json.JSONDecodeError, which is a subclass of ValueError. The error object has three attributes beyond the message string: msg, which is the description of the problem; doc, which is the full input string that failed to parse; and pos, which is the character position in doc where the error was found. It also provides lineno and colno as pre-computed properties. This makes Python's JSON error handling particularly developer-friendly, because you can access the raw input and position without parsing the message string.

The jq command-line tool uses its own JSON parser and reports errors in a format like: parse error (Invalid numeric literal at line 1, column 14). The jq messages are designed for human reading rather than machine parsing. When running jq in scripts, check the exit code: jq exits with code 0 on success and a non-zero code for parse failures. Piping standard error to a log file captures jq's error messages for later inspection.

When BOM characters cause position-0 failures

A UTF-8 BOM (Byte Order Mark) is the Unicode character U+FEFF, encoded as the three bytes 0xEF 0xBB 0xBF at the start of a file. It originated as a hint to text processing software about the encoding and byte order of a file. UTF-8 does not need a BOM because the encoding is unambiguous, but some tools, particularly Microsoft Notepad on Windows and older versions of Excel, write a UTF-8 BOM automatically when saving files in UTF-8 encoding.

When a BOM-prefixed file is read as a UTF-8 string in JavaScript using fs.readFileSync with the 'utf8' encoding, Node.js preserves the BOM character at the start of the resulting string as the JavaScript character U+FEFF. This character is invisible in most text displays, so the string appears to start with the correct opening brace of the JSON. But JSON.parse sees U+FEFF as the first character, finds it is not a valid JSON value character, and throws SyntaxError: Unexpected token in JSON at position 0.

The fix is to strip the BOM before parsing. A one-liner check for charCodeAt(0) === 0xFEFF and then calling slice(1) if true handles this correctly. The npm package strip-bom provides a more explicit and self-documenting version of this logic. Some JSON parsers and HTTP clients strip BOMs automatically, so the behavior can vary depending on how the file or response body reaches your parse call.

BOM issues are particularly common in organizations where developers work across Windows and Unix environments and JSON configuration files are edited on Windows machines. Adding a BOM strip to your JSON loading utility and documenting that your project uses UTF-8 without BOM — and ideally enforcing this with an EditorConfig rule — prevents the problem from recurring.

Logging raw input before parse attempts

The most valuable debugging practice for JSON parse errors is logging the raw input string before the parse attempt rather than only logging the error that parse throws. Once parse throws, you know the parse failed and you know the position, but you do not have access to the raw string from the error object alone. If you have not logged the raw string, you must reproduce the failure to see what the bad input was.

In server applications, this means storing the raw request body somewhere accessible before the body parser runs. In Express, the express.json() middleware can be configured with a verify callback that receives the raw buffer before parsing. This callback is the right place to log or store the raw body for later inspection in error handlers. In Fastify, the addContentTypeParser method serves a similar purpose.

For background jobs that consume messages from a queue, log the raw message payload at the start of each job, before any parsing. Message queue systems often do not store message bodies after delivery, so if a bad message is consumed and the job crashes, the body is gone unless you logged it. Structured logging with a field like rawPayload: rawMessage.substring(0, 500) gives you the first 500 characters of the bad payload in a searchable log store.

For file-based JSON loading, include the file path in parse error messages. SyntaxError: Unexpected token at position 34 is actionable when you know which file it refers to. Without the file path, you cannot locate the problem. A simple utility function that takes a file path, reads it, strips the BOM if present, and wraps the parse call with the file path in the error message handles all of these concerns in one place and can be used throughout the codebase to ensure consistent, debuggable JSON loading.

Quick fix checklist

  • Wrap JSON.parse in a try-catch that logs the raw input string
  • Include the source label (file path, API endpoint, queue name) in error messages
  • Check charCodeAt(0) === 0xFEFF and strip BOM before parsing files
  • Convert the position offset to line and column for multi-line JSON documents
  • Use jsonlint or jq from the CLI to get formatted error output for large files
  • When the error is at position 0, check for empty input or an HTML error page
  • Add response.text() before JSON.parse in fetch calls to preserve the raw body
  • Validate the Content-Type header equals application/json before calling parse

Related guides

Frequently asked questions

What does the position number in a JSON parse error mean?

The position number is a zero-based character offset from the start of the input string, pointing to the first character that violated the JSON grammar. For single-line JSON, this is the character index. For multi-line formatted JSON, you need to count newline characters before that position to compute the line number, and the characters after the last newline to compute the column.

Why does my JSON parse error say position 0?

A position 0 error means the very first character of the input is invalid. The most common causes are a BOM character (U+FEFF) written by a Windows text editor, an empty string passed to JSON.parse, or an HTML error page being passed instead of JSON. Check charCodeAt(0) to see the actual first character code, and verify the input is non-empty and starts with a JSON value character.

How do I get line and column numbers from a Node.js JSON parse error?

Node.js JSON parse errors provide a position offset, not line and column. To convert: count the number of newlines in rawInput.substring(0, position) to get the line number (plus 1 for 1-based lines), then find the last newline index before position and subtract it from position to get the column. Alternatively, run the file through python -m json.tool or jsonlint, which both report errors in line-and-column format.

How does the Python json.JSONDecodeError differ from JavaScript SyntaxError?

Python's json.JSONDecodeError includes the full input string in its doc attribute, the character position in pos, and pre-computed lineno and colno properties. JavaScript's SyntaxError from JSON.parse only includes the message string with the position embedded as text. Python's error is significantly more useful for debugging because you can access the raw input programmatically from the error object without having saved it separately.

Should I log the full raw input when a JSON parse error occurs?

Log enough to diagnose the problem: the source (file path, endpoint, queue), the position offset, and a context window of 50 to 100 characters centered on the error position. Logging the full input is ideal for debugging but may be impractical for large payloads or payloads containing sensitive data. A truncated log showing the first 200 characters and the context around the error position usually provides enough information.

What causes Expecting value: line 1 column 1 (char 0) in Python?

This Python json.JSONDecodeError occurs when the input string is empty or when the first character is not a valid JSON value starter. An empty string is the most common cause. Others include a BOM character at the start of the file, or a response body that was read as empty due to a connection error or incorrect encoding. Verify that the string has non-zero length and log its first few characters before calling json.loads.

Can JSON.parse recover from errors and return partial results?

No, JSON.parse is all-or-nothing. If the input contains any syntax error, it throws and returns nothing. There is no partial parsing mode in the standard JSON.parse API. For large or potentially malformed JSON, validate with a schema validator or a fault-tolerant JSON parser library before attempting to extract data. Libraries like json-source-map can parse with error recovery for developer tooling scenarios.

How do I find a JSON syntax error in a minified file thousands of characters long?

Use a command-line tool rather than trying to read the raw position offset. Run the file through jsonlint file.json or jq . file.json — both report errors in line-and-column format. For a minified single-line file, first pretty-print it with a formatter tool so that the output spans multiple lines, then run the linter on the formatted version. The error line and column in the formatted output directly correspond to the visible structure.

Why does response.json() in the Fetch API throw a SyntaxError?

response.json() internally calls JSON.parse on the response body text. If the server returns a non-JSON body — an HTML error page, a plain text message, or an empty body — JSON.parse throws a SyntaxError. To diagnose this, call response.text() instead of response.json() and log the resulting string. Check the response status code and Content-Type header to determine why the server returned non-JSON content.

All tools run in your browser. Your data never leaves your device. Last updated: 2026-05-05.