{"id":"l-js-4-4","courseId":"javascript-fundamentals","lang":"en","kind":"lesson","summary":"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.","html":"<p>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.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">JSON: data as text</h2>\n<p><strong>JSON</strong> (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.</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-run=\"js\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">JavaScript</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn code-run\" data-action=\"run\" data-lang=\"js\" data-index=\"0\" aria-label=\"Run this example\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z\"/></svg><span>Run</span></button><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> expenses</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  { id: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"exp-1\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, label: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"Coffee\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, amount: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">450</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, tags: [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"work\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] },</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  { id: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"exp-2\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, label: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"Train\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, amount: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1220</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, tags: [] },</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">];</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> text</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> JSON</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">stringify</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(expenses);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">console.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">log</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">typeof</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> text);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">console.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">log</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(text);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> back</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> JSON</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">parse</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(text);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">console.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">log</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(back[</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].label, back </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">===</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> expenses);</span></span></code></pre><div class=\"run-output\" data-run-output=\"0\" hidden></div></div>\n<p><code dir=\"ltr\">JSON.stringify</code> turns a value into a JSON string. <code dir=\"ltr\">JSON.parse</code> reads a JSON string and builds new values from it. The result is a fresh copy with the same contents, which is why <code dir=\"ltr\">back === expenses</code> is <code dir=\"ltr\">false</code>.</p>\n<p>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, <code dir=\"ltr\">JSON.stringify(value, null, 2)</code> indents the output by two spaces.</p>\n<h3 id=\"sec-2\" tabindex=\"-1\">What JSON cannot hold</h3>\n<p>JSON knows strings, numbers, booleans, <code dir=\"ltr\">null</code>, arrays and plain objects. Anything else is changed or lost on the way through:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-run=\"js\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">JavaScript</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn code-run\" data-action=\"run\" data-lang=\"js\" data-index=\"1\" aria-label=\"Run this example\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z\"/></svg><span>Run</span></button><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> tricky</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  when: </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">new</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> Date</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"2026-10-05T09:30:00Z\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">),</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  note: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">undefined</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  format</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">() { </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> \"hi\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; },</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  tags: </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">new</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> Set</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">([</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"work\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]),</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  ratio: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">NaN</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">};</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">console.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">log</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">JSON</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">stringify</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(tricky));</span></span></code></pre><div class=\"run-output\" data-run-output=\"1\" hidden></div></div>\n<p>Dates become ISO strings and do not come back as dates, <code dir=\"ltr\">undefined</code> properties and functions are dropped, Sets and Maps become empty objects, and <code dir=\"ltr\">NaN</code> becomes <code dir=\"ltr\">null</code>. Pocket avoids all of this by design: amounts are whole numbers, dates are stored as <code dir=\"ltr\">&quot;2026-10-05&quot;</code> strings, and categories are plain strings. Choosing JSON-friendly data from the start saves a lot of conversion code later.</p>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">Common mistake</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Parsing without a safety net</span></p>\n<p><code dir=\"ltr\">JSON.parse</code> throws a <code dir=\"ltr\">SyntaxError</code> when the text is not valid JSON, for example a half-written value or something another script stored under the same key. If that happens while your app starts up, the whole app fails to load. Any parse of text you did not just create yourself belongs inside <code dir=\"ltr\">try</code>/<code dir=\"ltr\">catch</code>.</p>\n</aside>\n<h2 id=\"sec-3\" tabindex=\"-1\">localStorage: a small store in the browser</h2>\n<p>Every website gets a small key-value store in each user’s browser, called <code dir=\"ltr\">localStorage</code>. It survives reloads and browser restarts, and it holds <strong>strings only</strong>:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">storage.js</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">localStorage.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">setItem</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"pocket.theme\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"dark\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">console.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">log</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(localStorage.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">getItem</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"pocket.theme\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)); </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// \"dark\"</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">console.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">log</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(localStorage.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">getItem</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"nothing-here\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)); </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// null</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">localStorage.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">removeItem</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"pocket.theme\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span></code></pre></div>\n<p>To store an array of expenses, turn it into JSON on the way in and parse it on the way out:</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 680 200\" role=\"img\" aria-labelledby=\"l-js-4-4-t1\"><defs><marker id=\"arrow-l-js-4-4-1\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path d=\"M0 0 L10 5 L0 10 z\" class=\"d-arrowhead\"/></marker></defs>\n  <title id=\"l-js-4-4-t1\">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.</title>\n  <rect class=\"d-box-primary\" x=\"10\" y=\"70\" width=\"150\" height=\"56\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"85\" y=\"96\" text-anchor=\"middle\">expenses</text>\n  <text class=\"d-label-muted\" x=\"85\" y=\"116\" text-anchor=\"middle\">array of objects</text>\n  <rect class=\"d-box\" x=\"265\" y=\"70\" width=\"150\" height=\"56\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"340\" y=\"96\" text-anchor=\"middle\">JSON string</text>\n  <text class=\"d-code\" x=\"340\" y=\"116\" text-anchor=\"middle\">'[{\"id\":…}]'</text>\n  <rect class=\"d-box-accent\" x=\"520\" y=\"70\" width=\"150\" height=\"56\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"595\" y=\"104\" text-anchor=\"middle\">localStorage</text>\n  <path class=\"d-arrow\" d=\"M160 82 L261 82\" marker-end=\"url(#arrow-l-js-4-4-1)\"/>\n  <path class=\"d-arrow\" d=\"M415 82 L516 82\" marker-end=\"url(#arrow-l-js-4-4-1)\"/>\n  <path class=\"d-arrow\" d=\"M516 114 L419 114\" marker-end=\"url(#arrow-l-js-4-4-1)\"/>\n  <path class=\"d-arrow\" d=\"M261 114 L164 114\" marker-end=\"url(#arrow-l-js-4-4-1)\"/>\n  <text class=\"d-code\" x=\"212\" y=\"60\" text-anchor=\"middle\">stringify</text>\n  <text class=\"d-code\" x=\"467\" y=\"60\" text-anchor=\"middle\">setItem</text>\n  <text class=\"d-code\" x=\"467\" y=\"150\" text-anchor=\"middle\">getItem</text>\n  <text class=\"d-code\" x=\"212\" y=\"150\" text-anchor=\"middle\">parse</text>\n</svg><figcaption>Saving and loading goes through JSON text</figcaption></figure>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"3\"><div class=\"code-head\"><span class=\"code-title\">storage.js</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> STORAGE_KEY</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> \"pocket.expenses.v1\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> saveExpenses</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">expenses</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) {</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  localStorage.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">setItem</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">STORAGE_KEY</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">JSON</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">stringify</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(expenses));</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> loadExpenses</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">() {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  try</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> text</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> localStorage.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">getItem</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">STORAGE_KEY</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> data</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> text </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">===</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> null</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> ?</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> JSON</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">parse</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(text);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> Array.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">isArray</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(data) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">?</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> data </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [];</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">catch</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [];</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p><code dir=\"ltr\">loadExpenses</code> 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 <code dir=\"ltr\">getItem</code> returns <code dir=\"ltr\">null</code> and we start with an empty list. Corrupted text makes <code dir=\"ltr\">JSON.parse</code> throw, and the <code dir=\"ltr\">catch</code> falls back to an empty list. Some browsers’ privacy modes block storage entirely, and that throws too, into the same <code dir=\"ltr\">catch</code>. Finally, <code dir=\"ltr\">Array.isArray</code> guards against valid JSON of the wrong shape. The <code dir=\"ltr\">.v1</code> in the key lets a future version of Pocket change the data format and migrate old data instead of misreading it.</p>\n<p>Then wire it up: load once at start-up, and save every time the data changes, right where you already call <code dir=\"ltr\">renderExpenses</code>:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"4\"><div class=\"code-head\"><span class=\"code-title\">app.js</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">let</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> expenses </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> loadExpenses</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">renderExpenses</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> setExpenses</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">next</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) {</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  expenses </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> next;</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  saveExpenses</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(expenses);</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  renderExpenses</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>Routing every change through one <code dir=\"ltr\">setExpenses</code> function means you cannot forget to save in one handler and remember in another.</p>\n<p>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 <code dir=\"ltr\">sessionStorage</code>, with the same methods, which is cleared when the tab is closed.</p>\n<aside class=\"callout callout-why\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><circle cx=\"12\" cy=\"12\" r=\"6\"/><circle cx=\"12\" cy=\"12\" r=\"2\"/></svg><span><span class=\"callout-kind\">Why this matters</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Know the limits</span></p>\n<p><code dir=\"ltr\">localStorage</code> is about 5 MB per site, synchronous (a huge save briefly blocks the page), and readable by any script running on your site. It is right for preferences and a personal app’s data. It is wrong for passwords, tokens or anything sensitive, and for data that must follow the user to another device, which needs a server.</p>\n</aside>\n<p>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.</p>\n","toc":[{"id":"sec-1","text":"JSON: data as text","level":2},{"id":"sec-2","text":"What JSON cannot hold","level":3},{"id":"sec-3","text":"localStorage: a small store in the browser","level":2}],"takeaways":["JSON is a text format for data; <code dir=\"ltr\">JSON.stringify</code> turns values into JSON text and <code dir=\"ltr\">JSON.parse</code> turns JSON text back into values.","JSON holds strings, numbers, booleans, null, arrays and plain objects; functions and <code dir=\"ltr\">undefined</code> are dropped, and dates become strings.","<code dir=\"ltr\">localStorage</code> stores strings only, per website, in the user’s browser; save with <code dir=\"ltr\">setItem(key, JSON.stringify(data))</code>.","Loading must survive a missing key (<code dir=\"ltr\">getItem</code> returns <code dir=\"ltr\">null</code>) and corrupted text (<code dir=\"ltr\">JSON.parse</code> throws), so wrap it in <code dir=\"ltr\">try</code>/<code dir=\"ltr\">catch</code> with a fallback."],"further":[{"title":"Working with JSON (MDN)","url":"https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Scripting/JSON"},{"title":"JSON.stringify() (MDN)","url":"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify"},{"title":"Window.localStorage (MDN)","url":"https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage"}],"quiz":[{"q":"<p><code dir=\"ltr\">localStorage.setItem(&quot;expenses&quot;, expenses)</code> is called with an array of objects. What is stored?</p>","options":[{"text":"The text <code dir=\"ltr\">&quot;[object Object],[object Object]&quot;</code>.","why":"Correct. Each object converts to “[object Object]” and the data is lost. Store <code dir=\"ltr\">JSON.stringify(expenses)</code> instead."},{"text":"The array, ready to use when you read it back.","why":"<code dir=\"ltr\">localStorage</code> only stores strings. Anything else is converted with <code dir=\"ltr\">String()</code> first."},{"text":"Nothing; <code dir=\"ltr\">setItem</code> throws for non-string values.","why":"It does not throw. It silently converts the value to a string, which is why the bug is easy to miss."}],"answer":0},{"q":"<p>What does <code dir=\"ltr\">JSON.stringify({ label: &quot;Tea&quot;, note: undefined, amount: 300 })</code> produce?</p>","options":[{"text":"<code dir=\"ltr\">{&quot;label&quot;:&quot;Tea&quot;,&quot;amount&quot;:300}</code>","why":"Correct. Properties whose value is <code dir=\"ltr\">undefined</code> (and functions) are left out, because JSON has no way to write them."},{"text":"<code dir=\"ltr\">{&quot;label&quot;:&quot;Tea&quot;,&quot;note&quot;:undefined,&quot;amount&quot;:300}</code>","why":"<code dir=\"ltr\">undefined</code> is not valid JSON, so <code dir=\"ltr\">stringify</code> never writes it inside an object."},{"text":"<code dir=\"ltr\">{&quot;label&quot;:&quot;Tea&quot;,&quot;note&quot;:null,&quot;amount&quot;:300}</code>","why":"<code dir=\"ltr\">stringify</code> drops <code dir=\"ltr\">undefined</code> properties rather than converting them to null. Only <code dir=\"ltr\">null</code> itself is written as null."}],"answer":0},{"q":"<p>Pocket loads with <code dir=\"ltr\">JSON.parse(localStorage.getItem(&quot;pocket.expenses&quot;))</code> on a first visit, when nothing was saved yet. What happens?</p>","options":[{"text":"It throws, because <code dir=\"ltr\">getItem</code> throws for unknown keys.","why":"<code dir=\"ltr\">getItem</code> returns <code dir=\"ltr\">null</code> for a missing key; it does not throw."},{"text":"It returns <code dir=\"ltr\">null</code>, because <code dir=\"ltr\">JSON.parse(null)</code> treats null as the JSON text “null”.","why":"Correct. The code then crashes later when it calls <code dir=\"ltr\">.map</code> on <code dir=\"ltr\">null</code>. Fall back with <code dir=\"ltr\">?? []</code> or an explicit check."},{"text":"It returns an empty array.","why":"Nothing creates an array here. You need to supply the empty-array fallback yourself."}],"answer":1}],"exercise":{"kind":"web","mode":"js","starter":{"js":"// Pocket's storage helpers. In this playground, localStorage works but starts\n// empty on every run; on a real page it keeps its data between visits.\nconst STORAGE_KEY = \"pocket.expenses.v1\";\n\nfunction saveExpenses(expenses) {\n  localStorage.setItem(STORAGE_KEY, expenses);\n}\n\nfunction loadExpenses() {\n  return JSON.parse(localStorage.getItem(STORAGE_KEY));\n}\n\nsaveExpenses([{ id: \"exp-1\", label: \"Coffee\", amount: 450 }]);\nconsole.log(localStorage.getItem(STORAGE_KEY));\n"},"solution":{"js":"// Pocket's storage helpers. In this playground, localStorage works but starts\n// empty on every run; on a real page it keeps its data between visits.\nconst STORAGE_KEY = \"pocket.expenses.v1\";\n\nfunction saveExpenses(expenses) {\n  localStorage.setItem(STORAGE_KEY, JSON.stringify(expenses));\n}\n\nfunction loadExpenses() {\n  try {\n    const text = localStorage.getItem(STORAGE_KEY);\n    const data = text === null ? [] : JSON.parse(text);\n    return Array.isArray(data) ? data : [];\n  } catch {\n    return [];\n  }\n}\n\nsaveExpenses([{ id: \"exp-1\", label: \"Coffee\", amount: 450 }]);\nconsole.log(localStorage.getItem(STORAGE_KEY));\n"},"checks":[{"id":"saves-json","test":"localStorage.clear();\nsaveExpenses([{ id: 'a', label: 'Tea', amount: 300 }]);\nconst raw = localStorage.getItem('pocket.expenses.v1');\ntry { const back = JSON.parse(raw); return Array.isArray(back) && back[0].label === 'Tea' && back[0].amount === 300; } catch { return false; }\n"},{"id":"round-trip","test":"localStorage.clear();\nconst data = [{ id: 'a', label: 'Tea', amount: 300 }, { id: 'b', label: 'Bus', amount: 270 }];\nsaveExpenses(data);\nconst back = loadExpenses();\nreturn Array.isArray(back) && back.length === 2 && back[1].label === 'Bus' && back[1].amount === 270 && back !== data;\n"},{"id":"missing","test":"localStorage.clear();\nconst out = loadExpenses();\nreturn Array.isArray(out) && out.length === 0;\n"},{"id":"corrupt","test":"localStorage.clear();\nlocalStorage.setItem('pocket.expenses.v1', '[{\"id\": \"a\", oops');\ntry { const out = loadExpenses(); return Array.isArray(out) && out.length === 0; } catch { return false; }\n"},{"id":"wrong-shape","test":"localStorage.clear();\nlocalStorage.setItem('pocket.expenses.v1', '{\"label\":\"not a list\"}');\ntry { const out = loadExpenses(); return Array.isArray(out) && out.length === 0; } catch { return false; }\n"}],"title":"Save and load Pocket safely","prompt":"<p>Pocket’s storage helpers lose data. Fix them, using the real <code dir=\"ltr\">localStorage</code> (in this playground it works but starts empty on every run):</p>\n<ul>\n<li>Fix <code dir=\"ltr\">saveExpenses(expenses)</code> so it stores the expenses as JSON under <code dir=\"ltr\">STORAGE_KEY</code>.</li>\n<li>Fix <code dir=\"ltr\">loadExpenses()</code> so it returns the saved array, and returns an empty array <code dir=\"ltr\">[]</code> 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.</li>\n</ul>\n","hints":["Log what the starter stores: <code dir=\"ltr\">[object Object]</code>. Wrap the data in <code dir=\"ltr\">JSON.stringify(...)</code> before <code dir=\"ltr\">setItem</code>.","<code dir=\"ltr\">getItem</code> returns <code dir=\"ltr\">null</code> when nothing was saved. Check for that before parsing.","Put the parse inside <code dir=\"ltr\">try { … } catch { return []; }</code>, and finish with <code dir=\"ltr\">return Array.isArray(data) ? data : [];</code>."],"explanation":"<p><code dir=\"ltr\">localStorage</code> only keeps strings, so the array has to be serialised with <code dir=\"ltr\">JSON.stringify</code> on the way in; otherwise it is converted with <code dir=\"ltr\">String()</code> and becomes <code dir=\"ltr\">[object Object]</code>. Loading guards against three different failures: a missing key (<code dir=\"ltr\">getItem</code> returns <code dir=\"ltr\">null</code>), text that is not JSON (<code dir=\"ltr\">JSON.parse</code> throws, which the <code dir=\"ltr\">catch</code> turns into an empty list), and JSON of the wrong shape (<code dir=\"ltr\">Array.isArray</code> rejects it). The same <code dir=\"ltr\">try</code> also catches browsers that block storage entirely, so a page load can never crash here.</p>\n","checkLabels":{"saves-json":"<code dir=\"ltr\">saveExpenses</code> stores the expenses as JSON text","round-trip":"Saved expenses load back unchanged","missing":"Loading with nothing saved returns []","corrupt":"Loading invalid JSON returns [] instead of throwing","wrong-shape":"Loading JSON that is not an array returns []"}},"words":715,"readMinutes":3}