Software & practice

Store, validate and remove browser data deliberately

Choose the storage mechanism and implement a reliable reset with a bounded JavaScript module and executed checks.

Metal organizer with separate compartments: a removable red tab labelled Anzeige, an untouched pale Andere Daten tab and a 7 Tage metal arch represent one bounded browser preference.

A website should remember a preferred spacing between table rows. On the next visit, the person sees the same comfortable overview. This small convenience sounds like two lines of JavaScript. A reliable implementation needs several additional decisions: what information belongs in the record, how long it remains valid, what happens when saving fails and how the application removes its own preference without damaging unrelated features on the same origin.

For a small, noncritical display preference, localStorage can be appropriate. It does not replace a user database, a secure login session or a backup. This guide develops a bounded table density preference. A complete storage module is combined with observable states, honest failure handling and a deliberate removal strategy. Technical sources and the brief legal discussion for Germany and the European Union were reviewed on 9 November 2026.

Define the purpose and the consequence of losing the value

The first question is not how many megabytes a browser permits. It is what a missing record means for the task. Losing a preferred row spacing can safely lead back to a sensible default. Losing the only copy of a substantial order draft is a different problem. The same API can therefore require very different recovery measures and explanations depending on the information entrusted to it.

Our learning example accepts two values: compact and comfortable. The demonstration buttons carry the German labels for compact and generous spacing. Without a valid personal choice, the table uses generous spacing. A failed write may still change the current display. The accompanying message must then explain that the view changed but the preference was not saved. This tells the person why the next visit might show a different arrangement.

A written purpose also prevents a preference record from gradually becoming an uncontrolled collection of information. Names, contact messages, payment details and login tokens do not belong alongside table density. Even an unnecessary random identifier can enable further uses. The display choice needs none of these additions. A minimal sufficient record makes technical inspection, legal assessment and a comprehensible reset easier to maintain together.

Compare storage mechanisms against the actual task

The following comparison supports architectural decisions. It is not a ranking of security. Each mechanism still needs its own assessment of access, possible loss and the actual information being retained. An attractive API name cannot replace that assessment.

MechanismAppropriate taskImportant boundary
Document memoryA temporary view stateNormally lost on a reload
sessionStorageSmall state within a page sessionAdditional separation by top level browsing context
localStorageA small string based preferenceNo built in application expiry
IndexedDBLarger structured data with transactionsRequires schema, migration and failure handling
Server with a suitable sessionAuthoritative account state or settings across devicesNeeds authentication and server side authorization

The MDN introduction to IndexedDB describes an asynchronous transactional database. That is a different responsibility from keeping a single small string. An image library or a substantial offline data set should not be recreated through ever larger JSON strings and frequent complete rewrites. Conversely, such a database would add unnecessary complexity to this deliberately limited example.

Cookies have a different relationship with HTTP: appropriately scoped cookies accompany matching requests. A value in localStorage is not automatically sent to the server merely because it exists. JavaScript can nevertheless read and transmit it. The absence of automatic transmission therefore establishes neither confidentiality nor anonymity. The scripts that actually run and the access they receive remain part of the assessment.

Separate origins, browser tabs and URL paths

An origin combines a scheme, host and port. Different paths on one origin do not establish separate Web Storage security compartments. A descriptive key helps avoid accidental naming collisions. It does not stop other executable JavaScript in the same accessible area from reading or replacing that key. An application installed below a subdirectory does not receive a protected storage drawer merely because its URL has a different path.

The localStorage reference also covers protocol separation and access failures. Opening a test document directly through a file: URL is not a dependable simulation of its eventual web deployment. The executable example therefore uses a small HTTP server. It does not need experiments on an unrelated production domain or any access to existing visitor data.

sessionStorage has an additional boundary around the top level browsing context. Reloading a tab does not itself end that page session. When a page has an opener, its initial session storage can be copied from that opener. Subsequent changes affect separate stores. Consequently, “a new tab is always empty” is an unreliable simplification. The browser checks here create independent pages without that opener relationship.

A narrow contract for one owned key

The module owns only az-journal-learning:v1:view. Its record contains exactly three fields: a schema version, an expiry timestamp and an allowed density choice. Unexpected fields are rejected along with unknown versions. This is an explicit policy for the example. A future extension should receive a reviewed migration rather than silently mixing assumptions about earlier and newer records.

Seven days is a chosen product rule, not a statutory retention period. Each successful choice starts that interval again. Reading does not extend it. The expiry is represented as an integer number of milliseconds. A separate limit on the input string bounds the processing of damaged records. The 2,048 character limit does not claim to describe a browser quota or the physical space occupied on a storage device.

The record is a display hint without authority. A person can alter it through developer tools. The device clock can also be wrong. For those reasons, its expiry must never decide whether a login remains valid or a payment is authorized. For this harmless preference, we explicitly accept that clock changes can affect validity. The reader also rejects an expiry that lies more than the chosen seven days ahead of the current clock.

German-labelled state matrix for a bounded browser preference: valid, missing, expired, invalid and unavailable. Current display and successful persistence are distinct. Removal targets only the owned key.
The explanation follows the executed module: apply a valid choice or use the default. Expired and invalid records trigger only a targeted removal attempt. Seven days is the validity rule chosen for this learning example. A failed write can change the current view while an earlier stored preference remains.

The complete storage module

The factory receives a function that obtains the storage object. This places even the access to window.localStorage inside failure handling. Catching only an exception from setItem would miss an exception thrown by that property getter. An injectable clock makes boundary checks possible without actually waiting for days. The normal invocation uses Date.now.

export const KEY = 'az-journal-learning:v1:view';
export const TTL = 7 * 24 * 60 * 60 * 1000;
const fallback = reason => ({choice: 'comfortable', reason});
const validTime = n => Number.isSafeInteger(n) && n >= 0 && n <= Number.MAX_SAFE_INTEGER - TTL;
export function createPreference(getStorage, now = Date.now) {
  function remove() {
    try { getStorage().removeItem(KEY); return true; }
    catch { return false; }
  }
  function read() {
    let raw;
    try { raw = getStorage().getItem(KEY); }
    catch { return fallback('unavailable'); }
    if (raw === null) return fallback('missing');
    let data;
    try {
      if (typeof raw !== 'string' || raw.length > 2048) throw Error();
      data = JSON.parse(raw);
      if (!data || Array.isArray(data) || typeof data !== 'object' ||
          Object.keys(data).sort().join(',') !== 'choice,expiresAt,version' ||
          data.version !== 1 || !['compact', 'comfortable'].includes(data.choice) ||
          !Number.isSafeInteger(data.expiresAt) || data.expiresAt < 0) throw Error();
    } catch { remove(); return fallback('invalid'); }
    const time = now();
    if (!validTime(time)) return fallback('unavailable');
    if (data.expiresAt <= time || data.expiresAt > time + TTL) {
      remove(); return fallback('expired');
    }
    return {choice: data.choice, reason: 'stored'};
  }
  function write(choice) {
    if (!['compact', 'comfortable'].includes(choice)) throw TypeError('choice');
    const time = now();
    if (!validTime(time)) return false;
    try {
      getStorage().setItem(KEY, JSON.stringify({version: 1, expiresAt: time + TTL, choice}));
      return true;
    } catch { return false; }
  }
  return {read, write, remove};
}

The result from read separates the display choice from the reason for that state. A missing record, an invalid record and unavailable storage can all produce the same generous default. They are still different situations for diagnostics and messaging. A full application can derive appropriate wording from them. It should avoid logging the entire record or creating identifying information that a harmless display choice does not need.

For damaged data, the module attempts to remove only its owned key. If that removal fails as well, the default view remains usable. A later read may encounter the same damaged entry again. That is preferable to claiming a cleanup that never occurred. A project with operational diagnostics can observe these outcomes separately. The core feature does not require an external monitoring service merely to work.

Keep display changes separate from successful persistence

The button expresses the desired current view. Whether that preference will survive a later visit is a second piece of information. The demonstration confirms a successful write as saved. After a failure, it explains that the choice was applied to this view but was not saved. Both statements describe steps that actually happened. Receiving a click is not sufficient evidence for a persistence success message.

An older stored value may remain after a failed overwrite. The next page load can therefore restore the earlier choice. The module does not secretly switch to another persistence mechanism to conceal this outcome. Its failure message is meaningful for precisely that reason. A more elaborate interface could offer a deliberate retry. An endless cycle of repeated writes would be disproportionate for a minor display convenience.

The reset action also has separate outcomes. The visible table can return to generous spacing even if persistent removal fails. That limit must be stated honestly. A successful removal refers to the owned preference key. It does not promise to erase all browser data, sign a person out or remove information from a server. Those broader tasks each require their own application contracts and implementation paths.

Complete executable HTML page for the module
<!doctype html><html lang="de"><meta charset="utf-8"><title>Zeilenabstand wählen</title>
<style>body{font:20px system-ui;margin:3rem;max-width:50rem}button{padding:1rem;margin:.4rem}table{border-collapse:collapse;width:100%}td,th{text-align:left;border-bottom:1px solid #aaa;padding:1rem}body[data-view=compact] td{padding:.3rem}</style>
<h1>Zeilenabstand</h1><button id="compact">Kompakt</button><button id="comfortable">Großzügig</button><button id="reset">Eigene Wahl entfernen</button><p id="status" role="status"></p>
<table><caption>Beispielübersicht</caption><tr><th>Aufgabe</th><th>Stand</th></tr><tr><td>Bild auswählen</td><td>Offen</td></tr><tr><td>Text prüfen</td><td>Offen</td></tr></table>
<script type="module">
import {KEY,createPreference} from './view-preference.mjs';
const preference=createPreference(()=>window.localStorage);
function render(state,message){document.body.dataset.view=state.choice;document.querySelector('#status').textContent=message;}
function refresh(){const state=preference.read();render(state,state.reason==='stored'?'Eigene Wahl geladen.':'Großzügiger Zeilenabstand.');}
for(const choice of ['compact','comfortable']) document.getElementById(choice).onclick=()=>{
 const saved=preference.write(choice);render({choice},saved?'Wahl gespeichert.':'Wahl für diese Ansicht angewendet, aber nicht gespeichert.');
};
document.querySelector('#reset').onclick=()=>{const removed=preference.remove();render({choice:'comfortable'},removed?'Eigene Wahl entfernt.':'Ansicht zurückgesetzt, gespeicherte Wahl konnte nicht entfernt werden.');};
window.addEventListener('storage',event=>{let storage;try{storage=window.localStorage;}catch{return;}if(event.storageArea===storage&&(event.key===KEY||event.key===null))refresh();});
window.addEventListener('pageshow',refresh);
window.addEventListener('focus',refresh);
refresh();
</script></html>

Synchronize two tabs without inventing a lock

The storage event reaches other eligible documents. The initiating document does not receive its own event. The button therefore updates the current display directly. Another tab reads the current validated record when a matching event arrives. Checking the storage area prevents an event for a different kind of Web Storage from being mistaken for this preference.

The listener handles the owned key and a null event key caused by a complete clear elsewhere. It does not perform such a complete clear itself. The example also rereads when the page is shown or receives focus. It does not run a timer that changes an already visible page at the exact expiry millisecond. An expired choice is noticed on the next relevant read. A product needing immediate expiry would have to add and test its own scheduling behavior, including recovery after background suspension.

The HTML Standard section on Web Storage warns authors against assuming a reliable lock between concurrent contexts. Two actions can overtake each other. For one display preference, accepting the last effective written record is reasonable. That assumption is unsuitable for invoice numbering or reservations. Adding a timestamp field does not turn several independent reads and writes into an atomic transaction.

Expiry and removal are different operations

Web Storage does not automatically remove a record because it contains our expiresAt field. The application evaluates that field and then attempts removal. If nobody opens the site again, the record may remain for longer. An application validity period is therefore different from a guaranteed physical deletion deadline. That distinction becomes especially important when a business needs evidence of removal for personal information.

Calling removeItem(KEY) fits the owned preference. Calling clear() can instead remove every value in that storage area and disrupt unrelated modules sharing the origin. Broad loops over key prefixes can also be dangerous without documented ownership. A later migration should identify exact old keys in a reviewed list. Resetting a table preference does not justify general browser cleanup or deleting another feature's state.

Browsers can limit storage and people can remove it themselves. The overview of quotas and eviction describes differing mechanisms. The architectural consequence is straightforward: important information needs a recovery strategy beyond this single preference. A successful write does not ensure that the same value will exist on every device or at every later time.

Assess security boundaries and legal purpose separately

JavaScript running with access to the relevant area can generally read accessible preference records. JSON is not encryption. Adding a checksum does not transform a browser controlled value into an authoritative permission. Appropriate defenses against cross site scripting and restrained inclusion of third party scripts remain important. A choice of row spacing must never count as evidence of identity or permission to access another person's information.

In Germany, section 25 TDDDG addresses storing information on terminal equipment and accessing it. Its exceptions have specific conditions. The technical choice of storage does not by itself determine whether consent is required. A requested setting may need a different assessment from a recognition identifier used for additional purposes. This example therefore makes no blanket claim that every preference is exempt from consent requirements.

Whether personal data is also processed, which legal basis applies and what information must be supplied depend on actual operations. A harmless label does not remove a real connection to an identifiable person. This technical guide is not individual legal clearance. Document the purpose, scope, validity, accessing scripts and available reset path. Turning a preference into tracking requires a new assessment rather than continued reliance on the unchanged label “setting”.

What the executable example actually checked

The module and browser demonstration passed 78 checks. Module checks use a controlled clock and a small storage double. They cover both allowed values, the last valid millisecond, exact expiry, malformed JSON, additional fields, unknown versions and preservation of an unrelated key. Unavailable access, write errors and removal errors are injected deliberately. These are controlled failure cases with clear expected outcomes.

A real Edge browser also opens two pages on the same origin. Checks cover the immediate change in the initiating document, notification of the other document, reload behavior, targeted removal and separate session storage in independently opened pages. An additional injected access denial checks that the display remains usable and the failure message stays honest. This injection does not claim coverage of every real privacy configuration.

The results do not include exhausting actual browser capacity, restarting a browser or testing every browser vendor. A production acceptance plan should cover the devices and settings actually supported by the product. The count of passed checks describes specific verified expectations for this example. It is neither a general quality score nor a security certification for the underlying storage API.

Checklist for adopting the pattern on a real website

Before release, describe the expected experience as well as the API result. With no entry, spacing remains generous. After a compact choice, reloading restores compact spacing. After expiry, a read restores the default. A failed write changes the current view with an accurate message. A failed removal must leave open the possibility that the previous stored choice returns later. These expectations make useful acceptance cases because they connect the storage contract to an observable visitor action.

A future schema change needs an explicit migration decision. Decide whether an earlier choice has an unambiguous translation, identify the exact old keys owned by this feature and verify what remains after migration. Moving the site to another origin does not automatically carry the preference across. For row spacing, asking for the choice again may be sufficient. Synchronizing it across devices would be a separate feature involving identity and additional data flows. It should not be added casually through a recognition parameter.

  • Write down the purpose of each field and omit unnecessary information.
  • Provide a sensible default when no valid choice is available.
  • Name owned keys clearly and preserve unrelated values.
  • Treat access, reading, writing and removal as possible failure points.
  • Validate the schema, permitted values and input size explicitly.
  • Distinguish application validity from actual cleanup.
  • Keep the current interface usable when persistence fails.
  • Read other tabs' changes through matching storage events.
  • Never derive authorization or indispensable business facts from a display preference.
  • Review legal purpose and actual data flows again before adding new uses.

A useful convenience feature demonstrates its reliability when stored data is missing or no longer fits. The spacing choice stays understandable. The application respects its ownership boundary. Failed operations are described accurately. This produces a maintainable small browser preference without confusing it with a secure session, a database or a comprehensive erasure function.

Let’s move forward

What would you like to change?

An idea, a specific challenge or your next step with AI. We’re listening.

Let’s talk