JSON.parse Error Handling – Safe Patterns in JavaScript

JSON.parse() throws a SyntaxError synchronously when given invalid input. If you do not catch it, the error propagates up the call stack and can crash your application or produce a blank UI. This article covers the most practical patterns for handling this error correctly.

The basic try/catch

try {
    const data = JSON.parse(jsonString);
    // use data safely here
} catch (error) {
    console.error('JSON parse failed:', error.message);
    // handle the error: show a message, use a default, etc.
}

This is the minimum. error.message will contain the parser's error description, which includes the character position of the problem — for example, Unexpected token '}' at position 42.

A reusable safe-parse function

If you parse JSON in many places, a helper function prevents repetition:

function tryParseJSON(str, fallback = null) {
    try {
        return JSON.parse(str);
    } catch {
        return fallback;
    }
}

// Usage
const config = tryParseJSON(localStorage.getItem('config'), {});
const items  = tryParseJSON(response.body, []);

The fallback parameter lets each call site specify a sensible default, making the behavior explicit rather than always returning null.

When to throw vs when to recover

Whether to silently recover or re-throw depends on where the invalid JSON comes from:

Recover silently for optional user preferences

If the JSON comes from localStorage or user input and a default is acceptable, return the fallback and log a warning — do not crash:

const prefs = tryParseJSON(localStorage.getItem('prefs'), defaultPrefs);

Throw with a clear message for external API responses

If a server is supposed to return valid JSON and it does not, that is a real error that should be surfaced:

async function fetchData(url) {
    const res = await fetch(url);
    const text = await res.text();

    let data;
    try {
        data = JSON.parse(text);
    } catch (e) {
        throw new Error(
            `API at ${url} returned invalid JSON.\n` +
            `Status: ${res.status}\n` +
            `Parse error: ${e.message}\n` +
            `Response preview: ${text.slice(0, 200)}`
        );
    }
    return data;
}

Including the raw response preview in the error makes debugging much faster — you can see immediately whether the server returned an HTML error page, a truncated response, or something else.

Getting line and column from the error

The built-in SyntaxError from JSON.parse includes the character offset but not a line number. To calculate the line number from the offset:

function parseJSONWithPosition(str) {
    try {
        return { value: JSON.parse(str), error: null };
    } catch (e) {
        // Extract position from the error message (not 100% portable across engines)
        const match = e.message.match(/position (\d+)/);
        const pos = match ? parseInt(match[1], 10) : -1;

        if (pos >= 0) {
            const before = str.slice(0, pos);
            const line = before.split('\n').length;
            const col = before.length - before.lastIndexOf('\n');
            return { value: null, error: `${e.message} (line ${line}, col ${col})` };
        }

        return { value: null, error: e.message };
    }
}

For production tooling, using the JSON Validator is more reliable — it shows the exact line and column with syntax highlighting in the browser.

TypeScript: narrowing the return type

function tryParseJSON<T = unknown>(str: string): T | null {
    try {
        return JSON.parse(str) as T;
    } catch {
        return null;
    }
}

// With a type assertion:
const user = tryParseJSON<{ name: string; age: number }>(raw);

Note that JSON.parse has no runtime type checking — even with a TypeScript generic, you should validate the shape of the parsed object before using it in production. Libraries like Zod or Valibot handle this cleanly.

Related articles