EduFlow
JavaScript Fundamentals
0%

Section 4 · JavaScript in the Browser

Saving Data: JSON and localStorage

16 min3 min readCode playground

Lesson at a glance

Turn data into JSON text and back with JSON.stringify and JSON.parse, know what JSON cannot hold, and save and load Pocket’s expenses with localStorage safely.

  1. 1JSON: data as text
  2. 2localStorage: a small store in the browser

No video here — just the real thing: a written lesson, runnable examples, a quiz and practice.

Add three expenses to Pocket, reload the page, and they are gone. Everything you built lives in JavaScript variables, and variables only last as long as the page. To remember data between visits you need to store it somewhere, and the simplest place a web page can store data is in the user’s own browser. First, though, the data has to become text.

JSON: data as text

JSON (JavaScript Object Notation) is a text format for data. It looks almost exactly like JavaScript object and array literals, which is no coincidence, and today it is the most common format for sending data between programs on the web.

JavaScript
const expenses = [
  { id: "exp-1", label: "Coffee", amount: 450, tags: ["work"] },
  { id: "exp-2", label: "Train", amount: 1220, tags: [] },
];

const text = JSON.stringify(expenses);
console.log(typeof text);
console.log(text);

const back = JSON.parse(text);
console.log(back[1].label, back === expenses);

JSON.stringify turns a value into a JSON string. JSON.parse reads a JSON string and builds new values from it. The result is a fresh copy with the same contents, which is why back === expenses is false.

JSON’s rules are stricter than JavaScript’s: property names must be in double quotes, strings use double quotes only, and trailing commas are not allowed. You rarely write JSON by hand, but when you do, those three rules cause most parse errors. For a readable version, JSON.stringify(value, null, 2) indents the output by two spaces.

What JSON cannot hold

JSON knows strings, numbers, booleans, null, arrays and plain objects. Anything else is changed or lost on the way through:

JavaScript
const tricky = {
  when: new Date("2026-10-05T09:30:00Z"),
  note: undefined,
  format() { return "hi"; },
  tags: new Set(["work"]),
  ratio: NaN,
};
console.log(JSON.stringify(tricky));

Dates become ISO strings and do not come back as dates, undefined properties and functions are dropped, Sets and Maps become empty objects, and NaN becomes null. Pocket avoids all of this by design: amounts are whole numbers, dates are stored as "2026-10-05" strings, and categories are plain strings. Choosing JSON-friendly data from the start saves a lot of conversion code later.

localStorage: a small store in the browser

Every website gets a small key-value store in each user’s browser, called localStorage. It survives reloads and browser restarts, and it holds strings only:

storage.js
localStorage.setItem("pocket.theme", "dark");
console.log(localStorage.getItem("pocket.theme")); // "dark"
console.log(localStorage.getItem("nothing-here")); // null
localStorage.removeItem("pocket.theme");

To store an array of expenses, turn it into JSON on the way in and parse it on the way out:

Saving: the expenses array is turned into a JSON string with JSON.stringify and stored with setItem. Loading: getItem returns the string, and JSON.parse turns it back into a new array. expenses array of objects JSON string '[{"id":…}]' localStorage stringify setItem getItem parse
Saving and loading goes through JSON text
storage.js
const STORAGE_KEY = "pocket.expenses.v1";

function saveExpenses(expenses) {
  localStorage.setItem(STORAGE_KEY, JSON.stringify(expenses));
}

function loadExpenses() {
  try {
    const text = localStorage.getItem(STORAGE_KEY);
    const data = text === null ? [] : JSON.parse(text);
    return Array.isArray(data) ? data : [];
  } catch {
    return [];
  }
}

loadExpenses is defensive on purpose, because it runs on every page load and must never crash the app. A first-time visitor has nothing saved, so getItem returns null and we start with an empty list. Corrupted text makes JSON.parse throw, and the catch falls back to an empty list. Some browsers’ privacy modes block storage entirely, and that throws too, into the same catch. Finally, Array.isArray guards against valid JSON of the wrong shape. The .v1 in the key lets a future version of Pocket change the data format and migrate old data instead of misreading it.

Then wire it up: load once at start-up, and save every time the data changes, right where you already call renderExpenses:

app.js
let expenses = loadExpenses();
renderExpenses();

function setExpenses(next) {
  expenses = next;
  saveExpenses(expenses);
  renderExpenses();
}

Routing every change through one setExpenses function means you cannot forget to save in one handler and remember in another.

To see what is stored, open DevTools, go to the Application panel in Chrome or Edge (Storage in Firefox), and expand Local Storage. You can read, edit and delete entries there, which is the quickest way to test what your app does with missing or broken data. There is also sessionStorage, with the same methods, which is cleared when the tab is closed.

You can now keep data across visits. The other big source of data is a server on the internet, and talking to one means waiting for an answer. Waiting is the subject of the next lesson.

Key takeaways

  • JSON is a text format for data; JSON.stringify turns values into JSON text and JSON.parse turns JSON text back into values.
  • JSON holds strings, numbers, booleans, null, arrays and plain objects; functions and undefined are dropped, and dates become strings.
  • localStorage stores strings only, per website, in the user’s browser; save with setItem(key, JSON.stringify(data)).
  • Loading must survive a missing key (getItem returns null) and corrupted text (JSON.parse throws), so wrap it in try/catch with a fallback.

Further reading

Check your understanding

3 questions

Question 1 of 3

localStorage.setItem("expenses", expenses) is called with an array of objects. What is stored?

Question 2 of 3

What does JSON.stringify({ label: "Tea", note: undefined, amount: 300 }) produce?

Question 3 of 3

Pocket loads with JSON.parse(localStorage.getItem("pocket.expenses")) on a first visit, when nothing was saved yet. What happens?

Practice

Code playground

Save and load Pocket safely

Pocket’s storage helpers lose data. Fix them, using the real localStorage (in this playground it works but starts empty on every run):

  • Fix saveExpenses(expenses) so it stores the expenses as JSON under STORAGE_KEY.
  • Fix loadExpenses() so it returns the saved array, and returns an empty array [] instead of crashing when nothing is saved, when the saved text is not valid JSON, or when it is valid JSON but not an array.
// Pocket's storage helpers. In this playground, localStorage works but starts
// empty on every run; on a real page it keeps its data between visits.
const STORAGE_KEY = "pocket.expenses.v1";

function saveExpenses(expenses) {
  localStorage.setItem(STORAGE_KEY, expenses);
}

function loadExpenses() {
  return JSON.parse(localStorage.getItem(STORAGE_KEY));
}

saveExpenses([{ id: "exp-1", label: "Coffee", amount: 450 }]);
console.log(localStorage.getItem(STORAGE_KEY));
Loading the editor…

Ctrl/⌘ + Enter to run · Runs in a sandbox in your browser. Nothing leaves your device.

Console

(no output)

Checks

  • saveExpenses stores the expenses as JSON text
  • Saved expenses load back unchanged
  • Loading with nothing saved returns []
  • Loading invalid JSON returns [] instead of throwing
  • Loading JSON that is not an array returns []