Storage Adapters

The Course
•
5 min read

Storage is a dependency, not a guarantee

The capstone intentionally keeps its saved books in memory. Rerunning or reloading starts fresh. To add persistence, inject a storage object with getItem and setItem rather than calling localStorage throughout the model. This creates one place to choose failure behavior.

On a normal origin, localStorage stores strings synchronously. Reads or writes may throw because of browser policy, quota, or unavailable storage. Parsed saved text may be invalid JSON or an obsolete schema. Validate restored data before putting it into state. An inaccessible store is different from a missing key, even if both lead to an empty initial list; communicate when saving is unavailable.

The isolated webpage labs do not have same-origin storage privileges. Use the controlled adapter below to practise. The downloadable HTTP-served project is a suitable place to add an actual localStorage adapter. Do not weaken the lab sandbox to make storage work.

Decide whether a failed write should undo the user’s in-memory action. For a reading queue, keeping the action and showing “not saved across reloads” is reasonable. For a payment workflow it would be an entirely different contract. A fallback must be visible; silent persistence failure creates false confidence.

Predict and trace

Write the output before running. Follow the values through each call; if your prediction differs, identify the first step that surprised you.

Predict, then run JavaScript
function writeSaved(storage, books) {
  try {
    storage.setItem("reading-queue:v1", JSON.stringify(books));
    return true;
  } catch {
    return false;
  }
}

const memory = new Map();
const storage = {
  setItem(key, text) {
    memory.set(key, text);
  }
};

console.log(writeSaved(storage, [{ id: "js", title: "Notes" }]));
console.log(JSON.parse(memory.get("reading-queue:v1"))[0].id);
console.log(
  writeSaved(
    {
      setItem() {
        throw new Error("Blocked");
      }
    },
    []
  )
);
Check your prediction
true
js
false

The Map is a test adapter, not persistence across program runs. A false result should drive visible feedback at the UI boundary.

Distinguish missing storage from malformed storage

A storage adapter crosses two boundaries: the storage operation can fail, and retrieved text can fail parsing or validation. A missing key is a normal empty state, while malformed text is a recovery decision. Neither should crash unrelated interaction unless the application’s policy explicitly requires it.

Version the storage key when its shape changes. Validate parsed records before exposing them as trusted application data. A JSON array alone is not enough evidence that it contains valid saved books.

Distinguish missing storage from malformed storage: trace JavaScript
function read(storage) {
  const text = storage.getItem("queue:v1");

  if (text === null) {
    return [];
  }

  const value = JSON.parse(text);

  if (!Array.isArray(value)) {
    throw new Error("Expected list");
  }

  return value;
}

console.log(
  read({
    getItem() {
      return null;
    }
  }).length
);
console.log(
  read({
    getItem() {
      return "[]";
    }
  }).length
);
Check your prediction
0
0

Both cases produce an empty array here, but they arrived through different paths. A later migration policy may need to distinguish absent data from existing serialized data.

Distinguish missing storage from malformed storage: contrast JavaScript
function write(storage, value) {
  try {
    storage.setItem("queue:v1", JSON.stringify(value));
    return true;
  } catch {
    return false;
  }
}

const storage = {
  setItem() {
    throw new Error("Quota exceeded");
  }
};

console.log(write(storage, [{ id: "a" }]));
Check your prediction
false

A storage write can fail even when serialization succeeds. The boolean gives the caller a recovery choice. It should not be converted into a claim that persistence succeeded.

Try the rule on new inputs

Test missing key, valid empty list, malformed JSON, wrong-shaped JSON and blocked writes. Decide which cases recover to empty and which show a warning. Keep the currently displayed saved state intact when a write fails.

Build it yourself

Exercise

Storage Adapters: practise

Write readList(storage), returning [] for a missing key but throwing for invalid JSON or a non-array body. Use getItem(“books”). Print the missing length and catch a wrong-shape error.

Your working space JavaScript
// getItem returns null when the key is missing.
Hint 1: the idea

Missing data is not the same as malformed saved data.

Hint 2: a first step

Parse only after checking for null, then check Array.isArray.

Show one solution
function readList(storage) {
  const text = storage.getItem("books");

  if (text === null) {
    return [];
  }

  const value = JSON.parse(text);

  if (!Array.isArray(value)) {
    throw new Error("Invalid saved list");
  }

  return value;
}

console.log(
  readList({
    getItem() {
      return null;
    }
  }).length
);

try {
  readList({
    getItem() {
      return "42";
    }
  });
} catch (error) {
  console.log(error.message);
}
0
Invalid saved list

This helper checks only the outer array. Connect the book decoder before accepting its records. Test a storage read that throws and decide how the caller should tell the user.

Test a changed requirement

This helper checks only the outer array. Connect the book decoder before accepting its records. Test a storage read that throws and decide how the caller should tell the user.

Explain before moving on

What does a versioned key help you change later? Why is a Map double not proof that localStorage is available?

Was this article helpful?

Your response is saved on this device.