Working With JSON and the Filesystem in JavaScript
Working With JSON and the Filesystem in JavaScript
~13 Min. Lesezeit Zuletzt aktualisiert am August 8, 2026
We've already used JSON.parse/JSON.stringify several times – time for a deeper look, including a few pitfalls relevant to persisting our budget app.
JSON.stringify() in detail
const transaction = { description: 'Rent', amount: -850, category: 'Housing' };
console.log(JSON.stringify(transaction));
// '{"description":"Rent","amount":-850,"category":"Housing"}'
// With indentation for readable files (third parameter: number of spaces):
console.log(JSON.stringify(transaction, null, 2));The second parameter (replacer) lets you exclude or transform certain properties – useful for e.g. keeping internal, non-persisted fields out of the output:
const transaction = { description: 'Rent', amount: -850, internalCache: { irrelevant: true } };
console.log(JSON.stringify(transaction, ['description', 'amount']));
// '{"description":"Rent","amount":-850}' - internalCache excludedWhat JSON CANNOT represent
JSON is a plain text data format with a limited type range – some JavaScript values get LOST or UNEXPECTEDLY converted by JSON.stringify():
undefinedas an object property gets DROPPED ENTIRELY, in an array it becomesnull.functionvalues get DROPPED ENTIRELY.Dateobjects are automatically converted to an ISO string ('2026-01-31T00:00:00.000Z'), NOT automatically converted back when parsing -JSON.parse()returns just a string again!MapandSetbecome{}- their actual data is ENTIRELY lost.
Achtung: For our budget app, concretely: a date should be stored as a plain string ('2026-01-31'), as we've already done since chapter 4 – NOT as a Date object, whose round trip through JSON only recovers half the information.
JSON.parse() with a reviver: transforming values while parsing
const json = '{"description":"Rent","amount":-850}';
const transaction = JSON.parse(json, (key, value) => {
if (key === 'amount') {
return Math.round(value); // e.g. consistently round decimals
}
return value;
});
console.log(transaction); // { description: 'Rent', amount: -850 }The fs module in depth: ensuring directories exist
Before our budget app writes to a file, the target directory should exist – mkdir with the recursive option automatically creates missing intermediate directories and throws NO error if the directory already exists:
import { readFile, writeFile, mkdir } from 'node:fs/promises';
import { dirname } from 'node:path';
import { FileNotFoundError, BudgetAppError } from './errors.js';
export async function loadTransactions(path) {
try {
const content = await readFile(path, 'utf-8');
return JSON.parse(content);
} catch (error) {
if (error.code === 'ENOENT') {
throw new FileNotFoundError(path);
}
throw new BudgetAppError(`Could not read ${path}: ${error.message}`);
}
}
export async function saveTransactions(path, transactions) {
try {
await mkdir(dirname(path), { recursive: true }); // ensure the directory exists
await writeFile(path, JSON.stringify(transactions, null, 2), 'utf-8');
} catch (error) {
throw new BudgetAppError(`Could not write ${path}: ${error.message}`);
}
}node:path's dirname() extracts a path's directory portion (dirname('data/transactions.json') yields 'data') – another built-in Node.js module useful for file path manipulation.
A brief mention: more robust writing
Tipp: In production-grade systems, it's common to FIRST write to a temporary file and THEN rename it (instead of overwriting the target file directly) – that way, a crash MID-WRITE leaves the old, intact file in place instead of ending up in a half-written, corrupt state. Direct writing is fine for our learning budget app, but good to know for later, real projects.