DEV Community

Chintan Shah
Chintan Shah

Posted on

Introducing handlejson - Safe JSON Parsing Without the try-catch Spam

I built a tiny library called handlejson because I got tired of writing try-catch blocks for JSON.parse everywhere. Just shipped v0.2.0 today with some features I actually needed in real projects.

The Problem

How many times have you written this?

let data
try {
  data = JSON.parse(str)
} catch {
  data = null
}
Enter fullscreen mode Exit fullscreen mode

I wrote it hundreds of times. localStorage.getItem? try-catch. API response? try-catch. User input? try-catch. Every single time.

It works, but it's verbose. And there are other annoyances too - JSON.stringify throws on circular references, Date objects serialize to empty objects (useless), and there's no easy way to validate structure.

What handlejson Does

It's basically a wrapper around native JSON functions, but with all the annoying edge cases handled:

import { parse, stringify } from 'handlejson'

// Safe parsing - returns null on error, no try-catch needed
const user = parse(localStorage.getItem('user'))
const config = parse(invalidJson, { default: {} })

// Handles circular refs automatically
const obj = { a: 1 }
obj.self = obj
stringify(obj)  // '{"a":1,"self":"[Circular]"}'

// BigInt support
stringify({ value: BigInt(123) })  // '{"value":"123n"}'
Enter fullscreen mode Exit fullscreen mode

That's the basics. Small, simple, just makes JSON parsing less annoying.

New in v0.2.0

I released v0.1.0 a while back with the core functionality. Today I'm shipping v0.2.0 with three features I kept needing:

Date Handling

This was the most annoying one. Every API response with dates means writing custom revivers to convert ISO strings back to Date objects:

// The old way
const user = parse(jsonString, {
  reviver: (key, value) => {
    if (key.endsWith('At') && typeof value === 'string') {
      const date = new Date(value)
      return isNaN(date.getTime()) ? value : date
    }
    return value
  }
})
Enter fullscreen mode Exit fullscreen mode

Now it's just:

const user = parse(jsonString, { dates: true })
// Dates are automatically converted
Enter fullscreen mode Exit fullscreen mode

Same for stringifying - Date objects serialize to ISO strings instead of empty objects:

stringify({ createdAt: new Date() }, { dates: true })
// → '{"createdAt":"2023-01-01T10:00:00.000Z"}'
Enter fullscreen mode Exit fullscreen mode

Saves a lot of boilerplate. I was writing that reviver logic constantly.

Schema Validation

I kept hitting bugs where API responses had wrong types. A number field coming through as a string, that sort of thing. Instead of writing custom validation each time, you can pass a schema:

const schema = {
  name: 'string',
  age: 'number',
  active: 'boolean',
  address: {
    street: 'string',
    zip: 'number'
  }
}

const user = parse(apiResponse, { schema })
// Returns null if validation fails
Enter fullscreen mode Exit fullscreen mode

It's not full JSON Schema (that'd be overkill), just simple type checking. The validation happens after parsing, so you get useful errors like "Field 'age': expected number, got string" instead of cryptic parse failures.

Useful for catching bugs early, especially when working with APIs you don't control.

Stream Parsing

This one's for processing large JSON files. Most projects won't need it, but if you're dealing with big datasets:

import { parseStream } from 'handlejson'

const result = await parseStream(response.body, {
  onProgress: (parsed) => console.log('Loading...'),
  onError: (err) => console.error('Error:', err)
})

if (result.complete) {
  console.log('Data:', result.data)
}
Enter fullscreen mode Exit fullscreen mode

I added this because I was working on a feature that needed to process multi-MB JSON files. Loading everything into memory first wasn't ideal.

Real Example

Here's a common pattern I see (and wrote myself many times):

// Before
function loadUser() {
  const stored = localStorage.getItem('user')
  if (!stored) return null

  try {
    const parsed = JSON.parse(stored)
    if (parsed.createdAt) {
      parsed.createdAt = new Date(parsed.createdAt)
    }
    return parsed
  } catch {
    return null
  }
}

// After
function loadUser() {
  return parse(localStorage.getItem('user'), {
    default: null,
    dates: true
  })
}
Enter fullscreen mode Exit fullscreen mode

Way cleaner. The new features make this even better - you can add schema validation if you want to be extra safe.

Why Build This?

There are other libraries that do similar things, but they're usually heavier or have more features than I need. I wanted something tiny that just handles the common edge cases.

It's ~2.8KB gzipped, zero dependencies, TypeScript native. Small enough that you can use it without thinking about bundle size.

Works in browser and Node.js. Same API everywhere.

Should You Use It?

If you're doing a lot of JSON parsing and tired of try-catch blocks everywhere, maybe. It's not revolutionary - just a quality-of-life improvement.

If you're happy with native JSON and don't mind the try-catch boilerplate, stick with that. No need to add dependencies for no reason.

But if you do a lot of JSON parsing (localStorage, API responses, user input), it cleans up your code quite a bit.

Try It

npm install handlejson
Enter fullscreen mode Exit fullscreen mode

The v0.2.0 release is on npm now. If you want to try it, that's the latest version with all the new features.

All the new stuff is opt-in, so existing code using handlejson (if anyone is) won't break. The new features are optional options.

What do you think? Do you write try-catch blocks for JSON.parse a lot, or am I the only one bothered by this? Always curious what other developers think about this stuff.

Top comments (1)

Collapse
 
onlineproxyio profile image
OnlineProxy

handlejson is a tiny, zero‑dep upgrade over native JSON-safe parse, circulars, Dates, light schema, streaming-without the kitchen‑sink vibes of superjson/devalue. Compared to a DIY safeParse, it bundles the stuff you keep re‑writing with a bit of overhead. ParseStream plays nice with Web Streams and Node, reports chunk‑level progress, handles full docs and relies on the underlying back‑pressure. The schema is intentionally minimal and fast, with pollution guards already in place, v1.0 targets stable stringify, bigint/date matchers, NDJSON, guardrails, and broader runtimes.