Przejdź do treści

Temat 25 · 2 × 45 min

Błędy, JSON i localStorage

Do tej pory błąd oznaczał czerwony napis w konsoli i zatrzymany skrypt. W prawdziwej aplikacji błędy są normalną częścią pracy: użytkownik wpisze literę zamiast liczby, serwer odpowie śmieciami, pamięć przeglądarki będzie pełna. Ta lekcja uczy trzech rzeczy, które zawsze występują razem: jak przechwycić i zgłosić błąd (try…catch, throw), jak zamienić dane na tekst i z powrotem (JSON) oraz jak zapisać je w przeglądarce, żeby przetrwały odświeżenie strony (localStorage). Razem dają „backend bez backendu” – aplikację, która pamięta stan bez serwera.

Rodzaje błędów i jak je czytać

Gdy silnik nie może wykonać instrukcji, rzuca (throw) wyjątek – obiekt błędu – i przerywa bieżący kod. Jeśli nic go nie przechwyci, trafia do konsoli jako Uncaught …. Wbudowane typy, z którymi spotkasz się najczęściej:

TypKiedyTypowy komunikat
SyntaxErrorkod nie da się sparsować (brak nawiasu, zły JSON)Unexpected token '}'
ReferenceErrorużycie nieistniejącej nazwycena is not defined
TypeErroroperacja na złym typie (metoda na undefined, zmiana const)Cannot read properties of null (reading 'value')
RangeErrorwartość poza zakresemInvalid array length, toFixed() digits argument must be between 0 and 100

Komunikat ma trzy części: nazwę typu, opis i stos wywołań (stack trace) – listę funkcji od miejsca błędu do początku, z numerami linii. Czytaj od góry: pierwsza linia stosu to miejsce, gdzie błąd powstał. Cannot read properties of null (reading 'value') niemal zawsze znaczy: getElementById nie znalazł elementu (literówka w id albo skrypt wykonał się przed HTML).

rodzaje-bledow.js
// każdy błąd łapiemy osobno, żeby zobaczyć wszystkie
const proby = [
  () => nieistniejaca + 1,
  () => null.value,
  () => (123.45).toFixed(200),
  () => JSON.parse('{ zly json }'),
];

for (const proba of proby) {
  try {
    proba();
  } catch (blad) {
    console.log(`${blad.name}: ${blad.message}`);
  }
}
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik

try…catch…finally

Blok try zawiera kod, który może się nie udać. Gdy w nim poleci wyjątek, wykonanie natychmiast przeskakuje do catch (reszta try jest pomijana), a w zmiennej po catch ląduje obiekt błędu. finally wykonuje się zawsze – po udanym try, po catch, a nawet gdy w środku jest return. Służy do sprzątania: ukrycia spinnera, odblokowania przycisku, zamknięcia połączenia.

Sprawdź się

Co wypisze ten kod?

function test() {
try {
  console.log('A');
  return 'wynik';
} catch {
  console.log('B');
} finally {
  console.log('C');
}
}
console.log(test());
try-catch-finally.js
function parsujLiczbe(tekst) {
  try {
    console.log('Próbuję:', tekst);
    const liczba = Number(tekst);
    if (Number.isNaN(liczba)) throw new Error(`„${tekst}” nie jest liczbą`);
    console.log('Udało się:', liczba);
    return liczba;
  } catch (blad) {
    console.log('Przechwycono:', blad.message);
    return 0;                    // wartość awaryjna – program działa dalej
  } finally {
    console.log('finally – zawsze, np. odblokuj przycisk');
  }
}

parsujLiczbe('42');
parsujLiczbe('abc');
console.log('Skrypt dalej działa');
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik

Od ES2019 zmienną w catch można pominąć (catch {), jeśli jej nie potrzebujesz. Blok try może mieć samo catch, samo finally lub oba.

Obiekt błędu

Każdy błąd ma name (typ), message (opis) i stack (stos jako napis). Do użytkownika pokazuj tylko message – i to przemyślane; do konsoli (console.error(blad)) cały obiekt, bo wtedy DevTools pokaże klikalny stos.

throw – zgłaszanie własnych błędów

throw rzuca dowolną wartość, ale rzucaj zawsze obiekt Error (lub jego podklasę) – tylko on ma stack, a narzędzia i biblioteki na to liczą. Rzucaj, gdy funkcja dostała dane, z którymi nie może zrobić nic sensownego: zamiast cicho zwracać NaN czy undefined, głośno powiedz, co jest nie tak. Decyzję, jak zareagować (komunikat, wartość domyślna, ponowna próba), zostaw temu, kto wywołał.

throw.js
function obliczRate(koszt, liczbaRat) {
  if (typeof koszt !== 'number' || koszt < 0) {
    throw new TypeError('Koszt musi być liczbą nieujemną');
  }
  if (!Number.isInteger(liczbaRat) || liczbaRat < 1) {
    throw new RangeError('Liczba rat musi być całkowita i co najmniej 1');
  }
  return Math.round((koszt / liczbaRat) * 100) / 100;
}

console.log(obliczRate(900, 4));

for (const [k, r] of [[900, 0], ['900', 3], [100, 3]]) {
  try {
    console.log(`Rata: ${obliczRate(k, r)} zł`);
  } catch (e) {
    console.log(`${e.name} → ${e.message}`);
  }
}
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik

Własne klasy błędów

Gdy aplikacja rozróżnia rodzaje problemów (dane użytkownika vs błąd sieci vs brak uprawnień), definiujesz własną klasę dziedziczącą po Error. W catch sprawdzasz instanceof i reagujesz inaczej na każdą. Składnię class … extends poznasz dokładnie w lekcji o klasach; tu wystarczy wzorzec do skopiowania.

wlasny-blad.js
class BladWalidacji extends Error {
  constructor(message, pole) {
    super(message);              // ustawia message i stack
    this.name = 'BladWalidacji'; // zamiast domyślnego „Error”
    this.pole = pole;            // dodatkowa informacja: które pole formularza
  }
}

function sprawdzWiek(tekst) {
  const wiek = Number(tekst);
  if (tekst.trim() === '' || Number.isNaN(wiek)) throw new BladWalidacji('Wpisz liczbę', 'wiek');
  if (wiek < 13 || wiek > 120) throw new BladWalidacji('Wiek musi być z zakresu 13–120', 'wiek');
  return wiek;
}

for (const wejscie of ['17', 'abc', '7']) {
  try {
    console.log('OK:', sprawdzWiek(wejscie));
  } catch (e) {
    if (e instanceof BladWalidacji) {
      console.log(`Pole „${e.pole}”: ${e.message}`);  // komunikat dla użytkownika
    } else {
      throw e;                                          // nie nasz błąd – puść dalej
    }
  }
}
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik

Zwróć uwagę na throw e w else: łapiemy tylko błędy walidacji, które umiemy pokazać użytkownikowi. Literówka w kodzie (ReferenceError) nie ma być maskowana komunikatem „Wpisz liczbę” – ma dotrzeć do konsoli.

JSON – dane jako tekst

JSON (JavaScript Object Notation) to tekstowy format zapisu danych: obiekty, tablice, napisy, liczby, true/false/null. Wygląda jak literał obiektu w JS, ale ma surowsze zasady – to format wymiany, który czytają wszystkie języki (PHP, Python, Java…). Dostaniesz go z każdego API, zapiszesz w nim stan aplikacji, w nim są pliki konfiguracyjne (package.json).

Literał JSJSON
klucze bez cudzysłowów: { imie: 'Ala' }klucze zawsze w podwójnych cudzysłowach: {"imie": "Ala"}
napisy w '…' lub "…"napisy tylko w "…"
przecinek po ostatnim elemencie dozwolonyprzecinek końcowy to błąd
komentarze //brak komentarzy
undefined, funkcje, Date, NaNtylko: obiekt, tablica, napis, liczba, true, false, null

JSON.stringify – obiekt → tekst

stringify.js
const uczen = {
  imie: 'Ala',
  oceny: [5, 4],
  aktywny: true,
  opiekun: null,
  // poniższe JSON gubi lub zmienia:
  srednia: undefined,
  policz() { return 1; },
  data: new Date('2026-10-20T10:00:00Z'),
  limit: NaN,
};

const tekst = JSON.stringify(uczen);
console.log(tekst);
console.log(typeof tekst);

// czytelnie, z wcięciem 2 spacji – do debugowania i plików
console.log(JSON.stringify({ a: 1, b: [1, 2] }, null, 2));

// tylko wybrane pola: tablica kluczy jako drugi argument
console.log(JSON.stringify(uczen, ['imie', 'aktywny']));
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik

Co się stało: undefined i metoda zniknęły, Date stała się napisem ISO, NaN stało się null. Po odczytaniu z JSON data będzie zwykłym napisem – trzeba ją odtworzyć przez new Date(tekst). O tym, dlaczego JSON.parse(JSON.stringify(x)) to kiepska kopia głęboka, była mowa w lekcji o referencjach.

JSON.parse – tekst → obiekt

Sprawdź się

Co wypisze ten kod?

const dane = JSON.parse('{"a": 1, "b": [true, null]}');
console.log(dane.a, dane.b.length, typeof dane);

JSON.parse jest surowy: na niepoprawnym tekście rzuca SyntaxError. Dlatego każde JSON.parse danych, których nie napisałeś sam (z serwera, z localStorage, z pola tekstowego), owijaj w try…catch. Przypadki brzegowe warto znać na pamięć:

parse.js
console.log(JSON.parse('{"imie":"Ala","oceny":[5,4]}').oceny[0]);  // 5
console.log(JSON.parse('[1, 2, 3]').length);                         // 3
console.log(JSON.parse('"tekst"'), JSON.parse('42'), JSON.parse('true')); // tekst 42 true

// null jest zamieniane na napis "null", a to poprawny JSON → wynik null
console.log(JSON.parse(null));                                       // null

const zle = ["{ imie: 'Ala' }", "{'a': 1}", '{"a": 1,}', '', undefined];
for (const tekst of zle) {
  try {
    JSON.parse(tekst);
    console.log('OK:', tekst);
  } catch (e) {
    console.log(`${e.name} dla ${JSON.stringify(tekst)}`);
  }
}
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik

To ważne dla localStorage: getItem zwraca null, gdy klucza nie ma, a JSON.parse(null) grzecznie daje null – nie błąd. Ale pusty napis albo uszkodzone dane rzucą SyntaxError. Stąd wzorzec parsujBezpiecznie:

parsuj-bezpiecznie.js
function parsujBezpiecznie(tekst, domyslna) {
  if (tekst === null || tekst === undefined) return domyslna;
  try {
    return JSON.parse(tekst);
  } catch (e) {
    console.log('Uszkodzone dane, używam domyślnych:', e.message);
    return domyslna;
  }
}

console.log(parsujBezpiecznie('[1,2]', []));
console.log(parsujBezpiecznie(null, []));        // brak klucza w storage
console.log(parsujBezpiecznie('{oops', []));     // uszkodzone
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik

Dla dat przydaje się drugi argument JSON.parse – reviver, funkcja wywoływana dla każdej pary klucz–wartość, która może ją podmienić:

reviver.js
const tekst = '{"tytul":"Egzamin","termin":"2027-01-11T08:00:00.000Z"}';
const zadanie = JSON.parse(tekst, (klucz, wartosc) =>
  klucz === 'termin' ? new Date(wartosc) : wartosc
);
console.log(zadanie.termin.getFullYear(), typeof zadanie.tytul);
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik

localStorage – pamięć przeglądarki

localStorage to wbudowany magazyn klucz–wartość, który przeglądarka trzyma na dysku, osobno dla każdej witryny (origin: protokół + domena + port). Dane przetrwają odświeżenie, zamknięcie karty i restart komputera – aż sam je usuniesz albo użytkownik wyczyści dane strony. Limit to zwykle ok. 5 MB na witrynę. sessionStorage ma identyczne API, ale żyje tylko do zamknięcia karty.

MetodaCo robi
localStorage.setItem(klucz, wartosc)zapisuje; wartość jest zamieniana na napis
localStorage.getItem(klucz)zwraca napis albo null, gdy klucza nie ma
localStorage.removeItem(klucz)usuwa jeden klucz
localStorage.clear()usuwa wszystko z tej witryny
localStorage.length, localStorage.key(i)liczba kluczy i i-ty klucz

Dwie konsekwencje, które powodują większość błędów:

  1. Przechowywane są tylko napisy. setItem('licznik', 5) zapisze "5"; po odczycie getItem('licznik') + 1 da "51". Obiekt zapisany wprost stanie się "[object Object]". Dlatego wszystko oprócz prostych napisów przepuszczaj przez JSON: setItem(k, JSON.stringify(obj)) i JSON.parse(getItem(k)).
  2. Dostęp może rzucić wyjątek. W trybie prywatnym niektórych przeglądarek, przy pełnej pamięci (QuotaExceededError) albo w ramce z zablokowanym dostępem (SecurityError) samo localStorage.setItem wyrzuci błąd. Opakuj dostęp w funkcje z try…catch, a aplikacja przeżyje bez pamięci.
storage-wzorzec.js
// Uruchom ten kod w konsoli DevTools na dowolnej stronie (F12 → Console)
// albo wklej do pliku .html – w ramce „Uruchom” localStorage jest zablokowany.
localStorage.setItem('motyw', 'ciemny');
console.log(localStorage.getItem('motyw'));      // 'ciemny'
console.log(localStorage.getItem('brak'));       // null

localStorage.setItem('licznik', 5);
console.log(typeof localStorage.getItem('licznik')); // 'string'!
console.log(localStorage.getItem('licznik') + 1);    // '51' – konkatenacja
console.log(Number(localStorage.getItem('licznik')) + 1); // 6

const zadania = [{ tekst: 'Nauczyć się JSON', zrobione: false }];
localStorage.setItem('zadania', JSON.stringify(zadania));
const odczytane = JSON.parse(localStorage.getItem('zadania'));
console.log(odczytane[0].tekst, odczytane[0].zrobione);

localStorage.removeItem('licznik');

Bezpieczne opakowanie

Zamiast wywoływać localStorage w dziesięciu miejscach, napisz dwie funkcje: zapisz(klucz, wartosc) i odczytaj(klucz, domyslna). Jedno miejsce z JSON i try…catch – reszta aplikacji używa zwykłych obiektów.

magazyn.js
function zapisz(klucz, wartosc) {
  try {
    localStorage.setItem(klucz, JSON.stringify(wartosc));
    return true;
  } catch (e) {
    console.error('Nie udało się zapisać:', e.name, e.message); // np. QuotaExceededError
    return false;
  }
}

function odczytaj(klucz, domyslna) {
  try {
    const tekst = localStorage.getItem(klucz);
    return tekst === null ? domyslna : JSON.parse(tekst);
  } catch (e) {
    console.error('Nie udało się odczytać:', e.name, e.message); // SecurityError lub SyntaxError
    return domyslna;
  }
}

// użycie
const ustawienia = odczytaj('ustawienia', { motyw: 'jasny', rozmiar: 16 });
ustawienia.motyw = 'ciemny';
zapisz('ustawienia', ustawienia);

Dlaczego place zabaw w tej lekcji działają „zastępczo”

Plac zabaw na tej stronie uruchamia kod w ramce <iframe sandbox> o tzw. nieprzejrzystym pochodzeniu (opaque origin). Przeglądarka nie wie, do jakiej witryny należą dane, więc każde dotknięcie localStorage rzuca tam SecurityError. To nie błąd w kodzie – to ten sam przypadek, przed którym chroni try…catch wyżej. Place zabaw poniżej wykrywają go i przełączają się na magazyn w pamięci (zwykły obiekt), który znika przy ponownym uruchomieniu. Zobaczysz komunikat o trybie. Żeby zobaczyć prawdziwe zapamiętywanie po odświeżeniu, zapisz kod do pliku .html na dysku i otwórz go w przeglądarce – gotowy plik jest w ostatnim zadaniu.

Motyw zapamiętany w localStorage (z trybem zastępczym)
// Magazyn: prawdziwy localStorage, a gdy jest niedostępny – obiekt w pamięci z tym samym API
function zrobMagazyn() {
  try {
    const test = '__test__';
    localStorage.setItem(test, '1');
    localStorage.removeItem(test);
    document.getElementById('tryb').textContent = 'Tryb: prawdziwy localStorage – odśwież stronę, motyw zostanie.';
    return localStorage;
  } catch (e) {
    document.getElementById('tryb').textContent =
      `Tryb zastępczy (pamięć): ${e.name} – w tej ramce localStorage jest zablokowany. Zapisz kod do pliku .html, żeby działał naprawdę.`;
    const pamiec = {};
    return {
      getItem: (k) => (k in pamiec ? pamiec[k] : null),
      setItem: (k, v) => { pamiec[k] = String(v); },
      removeItem: (k) => { delete pamiec[k]; },
    };
  }
}

const magazyn = zrobMagazyn();
const stan = document.getElementById('stan');

function zastosuj(motyw) {
  document.body.classList.toggle('ciemny', motyw === 'ciemny');
  stan.textContent = `Aktualny motyw: ${motyw}`;
}

// odczyt przy starcie – gdy klucza nie ma, getItem daje null, więc ?? ustawia domyślny
let motyw = magazyn.getItem('motyw') ?? 'jasny';
zastosuj(motyw);

document.getElementById('przelacz').addEventListener('click', () => {
  motyw = motyw === 'jasny' ? 'ciemny' : 'jasny';
  magazyn.setItem('motyw', motyw);
  zastosuj(motyw);
});
Podgląd

„Backend bez backendu”: wzorzec stan → zapis → odtworzenie

Aplikacja, która pamięta dane bez serwera, zawsze ma ten sam szkielet:

  1. Stan w jednej zmiennej (tablica obiektów), jak w lekcji o stanie aplikacji.
  2. renderuj() – rysuje DOM z stanu (np. map().join('') z lekcji o metodach tablic).
  3. zapisz() – po każdej zmianie stanu localStorage.setItem(klucz, JSON.stringify(stan)).
  4. Start – stan = odczytaj(klucz, []), potem renderuj().

Każda akcja użytkownika robi trzy rzeczy w tej kolejności: zmień stan → zapisz → renderuj. Jeśli zapis pominiesz w jednym miejscu, po odświeżeniu znikną właśnie te zmiany – to najczęstszy błąd. Dlatego wiele osób łączy zapis z renderowaniem w jedną funkcję aktualizuj().

Ściąga

SkładniaCo robiUwaga
try { … } catch (e) { … }przechwyć wyjątek z trytylko kod synchroniczny w try
finally { … }zawsze na końcunawet po return i po błędzie
catch { … }bez zmiennej (ES2019)gdy błąd nieistotny – ale zaloguj!
throw new Error('opis')rzuć błądrzucaj obiekty Error, nie napisy
e.name, e.message, e.stacktyp, opis, stosdo użytkownika tylko message
class X extends Errorwłasny typ błędusuper(message); this.name = 'X'
e instanceof Xrozróżnij typ w catchinne błędy rzuć dalej: throw e
JSON.stringify(obj)obiekt → tekst JSONgubi undefined, funkcje; Date → napis
JSON.stringify(obj, null, 2)z wcięciemdo debugowania i plików
JSON.parse(tekst)tekst → obiektzły tekst → SyntaxError; parse(null) → null
JSON.parse(t, reviver)podmień wartości przy odczycienp. napis → Date
localStorage.setItem(k, v)zapisz napisobiekt → najpierw JSON.stringify
localStorage.getItem(k)napis lub nullliczby wracają jako napisy
localStorage.removeItem(k), .clear()usuń klucz / wszystkotylko tej witryny
sessionStorageto samo API, żyje do zamknięcia karty
try { localStorage… } catchochrona przed SecurityError/QuotaExceededErrortryb prywatny, pełna pamięć, ramki

Zadania

Zadanie 1
łatwe

Bezpieczny odczyt ustawień

Napisz funkcję wczytajUstawienia(tekst), która przyjmuje napis z JSON (taki, jaki zwróciłby localStorage.getItem) i zwraca obiekt ustawień. Zasady: dla null zwraca domyślne { motyw: 'jasny', rozmiar: 16 }; dla uszkodzonego JSON wypisuje ostrzeżenie i zwraca domyślne; dla poprawnego JSON uzupełnia brakujące pola domyślnymi (spread). Przetestuj na: null, '{"motyw":"ciemny"}', '{zepsute'.

Pokaż rozwiązanie
wczytaj-ustawienia.js
const DOMYSLNE = { motyw: 'jasny', rozmiar: 16 };

function wczytajUstawienia(tekst) {
  if (tekst === null) return { ...DOMYSLNE };   // brak klucza – kopia domyślnych
  try {
    const zapisane = JSON.parse(tekst);
    return { ...DOMYSLNE, ...zapisane };         // zapisane nadpisują domyślne, reszta z domyślnych
  } catch (e) {
    console.log('Ostrzeżenie: uszkodzone ustawienia –', e.message);
    return { ...DOMYSLNE };
  }
}

console.log(wczytajUstawienia(null));
console.log(wczytajUstawienia('{"motyw":"ciemny"}'));
console.log(wczytajUstawienia('{zepsute'));
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik
Zadanie 2
średnie

Walidacja zamówienia z własnymi błędami

Zdefiniuj klasę BladZamowienia extends Error z dodatkowym polem kod ('PUSTE', 'ILOSC', 'BRAK_PRODUKTU'). Funkcja zlozZamowienie(koszyk, cennik) ma rzucać ten błąd, gdy: koszyk jest pusty; któraś pozycja ma ilosc niecałkowitą lub mniejszą niż 1; produktu nie ma w cenniku. Gdy wszystko gra, zwraca sumę. Osobna funkcja obsluz(koszyk) wywołuje zlozZamowienie w try…catch, wypisuje komunikat zależny od kod i puszcza dalej błędy innych typów.

Pokaż rozwiązanie
blad-zamowienia.js
class BladZamowienia extends Error {
  constructor(message, kod) {
    super(message);
    this.name = 'BladZamowienia';
    this.kod = kod;
  }
}

const cennik = { kabel: 20, mysz: 80 };

function zlozZamowienie(koszyk, cennik) {
  if (koszyk.length === 0) throw new BladZamowienia('Koszyk jest pusty', 'PUSTE');
  let suma = 0;
  for (const { produkt, ilosc } of koszyk) {
    if (!Number.isInteger(ilosc) || ilosc < 1) {
      throw new BladZamowienia(`Zła ilość dla „${produkt}”: ${ilosc}`, 'ILOSC');
    }
    if (!(produkt in cennik)) {
      throw new BladZamowienia(`Nie znamy produktu „${produkt}”`, 'BRAK_PRODUKTU');
    }
    suma += cennik[produkt] * ilosc;
  }
  return suma;
}

const komunikaty = {
  PUSTE: 'Dodaj coś do koszyka.',
  ILOSC: 'Popraw ilość – musi być liczbą całkowitą od 1.',
  BRAK_PRODUKTU: 'Jeden z produktów jest już niedostępny.',
};

function obsluz(koszyk) {
  try {
    console.log('Do zapłaty:', zlozZamowienie(koszyk, cennik), 'zł');
  } catch (e) {
    if (e instanceof BladZamowienia) {
      console.log(`[${e.kod}] ${komunikaty[e.kod]} (${e.message})`);
    } else {
      throw e; // np. TypeError z literówki – nie maskujemy
    }
  }
}

obsluz([{ produkt: 'kabel', ilosc: 2 }, { produkt: 'mysz', ilosc: 1 }]);
obsluz([]);
obsluz([{ produkt: 'kabel', ilosc: 1.5 }]);
obsluz([{ produkt: 'monitor', ilosc: 1 }]);
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik
Zadanie 3
średnie

Licznik odwiedzin i ostatnia wizyta

Zbuduj stronę, która po każdym otwarciu wypisuje: „To twoja N. wizyta. Ostatnio: data i godzina” (przy pierwszej wizycie: „Witaj po raz pierwszy!”). Stan trzymaj w localStorage pod jednym kluczem jako JSON { liczba, ostatnia }, gdzie ostatnia to data w formacie ISO (new Date().toISOString()). Dodaj przycisk „Wyzeruj”. Dostęp do magazynu opakuj w try…catch (w placu zabaw zobaczysz tryb zastępczy – prawdziwe liczenie sprawdzisz w pliku .html).

Pokaż rozwiązanie
Licznik odwiedzin
const KLUCZ = 'wizyty';

function odczytaj(klucz, domyslna) {
  try {
    const tekst = localStorage.getItem(klucz);
    return tekst === null ? domyslna : JSON.parse(tekst);
  } catch (e) {
    document.getElementById('tryb').textContent = `Tryb zastępczy: ${e.name} – zapisz kod do pliku .html, żeby licznik działał między odświeżeniami.`;
    return domyslna;
  }
}

function zapisz(klucz, wartosc) {
  try {
    localStorage.setItem(klucz, JSON.stringify(wartosc));
  } catch (e) {
    // w ramce bez localStorage po prostu nie zapisujemy – strona dalej działa
  }
}

const poprzednie = odczytaj(KLUCZ, null);
const komunikat = document.getElementById('komunikat');

if (poprzednie === null) {
  komunikat.textContent = 'Witaj po raz pierwszy!';
} else {
  // z JSON data wraca jako napis ISO – odtwarzamy obiekt Date, żeby ją sformatować
  const ostatnia = new Date(poprzednie.ostatnia);
  komunikat.textContent = `To twoja ${poprzednie.liczba + 1}. wizyta. Ostatnio: ${ostatnia.toLocaleString('pl-PL')}`;
}

zapisz(KLUCZ, {
  liczba: (poprzednie?.liczba ?? 0) + 1,
  ostatnia: new Date().toISOString(),
});

document.getElementById('wyzeruj').addEventListener('click', () => {
  try { localStorage.removeItem(KLUCZ); } catch {}
  komunikat.textContent = 'Wyzerowano – odśwież stronę.';
});
Podgląd
Zadanie 4
trudne

Lista zadań, która pamięta

Rozbuduj listę zadań z lekcji o tworzeniu elementów: dodawanie, oznaczanie „Wykonane” (przekreślenie), usuwanie – i zapis całego stanu w localStorage tak, by po odświeżeniu lista wróciła w tym samym stanie. Wymagania:

  • stan to tablica obiektów { id, tekst, zrobione } w jednej zmiennej; DOM jest zawsze rysowany ze stanu (renderuj()), nigdy odwrotnie,
  • każda akcja: zmień stan → zapisz() → renderuj(),
  • odczyt przy starcie przez bezpieczne odczytaj(klucz, []) z try…catch,
  • id generuj przez Date.now() albo crypto.randomUUID(),
  • tekst zadania wstawiaj przez textContent (nie innerHTML) – użytkownik może wpisać <b>.

Zapisz rozwiązanie jako kompletny plik zadania.html i otwórz z dysku – sprawdź, że po odświeżeniu (F5) i po zamknięciu karty zadania zostają, a w DevTools → Application → Local Storage widzisz klucz z JSON.

Pokaż rozwiązanie

Wersja do placu zabaw (z trybem zastępczym) i poniżej ten sam kod jako kompletny plik.

Lista zadań z localStorage
const KLUCZ = 'zadania';

function odczytaj(klucz, domyslna) {
  try {
    const tekst = localStorage.getItem(klucz);
    return tekst === null ? domyslna : JSON.parse(tekst);
  } catch (e) {
    document.getElementById('tryb').textContent = `Tryb zastępczy: ${e.name}. W pliku .html na dysku lista przetrwa odświeżenie.`;
    return domyslna;
  }
}

function zapisz(klucz, wartosc) {
  try {
    localStorage.setItem(klucz, JSON.stringify(wartosc));
  } catch (e) {
    console.error('Zapis nieudany:', e.name);
  }
}

// 1. stan – jedyne źródło prawdy
let zadania = odczytaj(KLUCZ, []);

// 2. render ze stanu
const lista = document.getElementById('lista');
function renderuj() {
  lista.innerHTML = '';
  for (const z of zadania) {
    const li = document.createElement('li');
    li.className = z.zrobione ? 'zrobione' : '';

    const tekst = document.createElement('span');
    tekst.textContent = z.tekst;           // bezpiecznie, nawet gdy wpisano <b>

    const wykonane = document.createElement('button');
    wykonane.textContent = z.zrobione ? 'Cofnij' : 'Wykonane';
    wykonane.addEventListener('click', () => przelacz(z.id));

    const usun = document.createElement('button');
    usun.textContent = 'Usuń';
    usun.addEventListener('click', () => usunZadanie(z.id));

    li.append(tekst, wykonane, usun);
    lista.append(li);
  }
  const zrobione = zadania.filter((z) => z.zrobione).length;
  document.getElementById('podsumowanie').textContent = `Zadań: ${zadania.length}, wykonanych: ${zrobione}`;
}

// 3. akcje: zmień stan → zapisz → renderuj (bez mutowania – nowe tablice/obiekty)
function aktualizuj(nowyStan) {
  zadania = nowyStan;
  zapisz(KLUCZ, zadania);
  renderuj();
}

function dodaj(tekst) {
  aktualizuj([...zadania, { id: Date.now(), tekst, zrobione: false }]);
}

function przelacz(id) {
  aktualizuj(zadania.map((z) => (z.id === id ? { ...z, zrobione: !z.zrobione } : z)));
}

function usunZadanie(id) {
  aktualizuj(zadania.filter((z) => z.id !== id));
}

document.getElementById('formularz').addEventListener('submit', (event) => {
  event.preventDefault();                 // formularz nie przeładowuje strony
  const pole = document.getElementById('nowe');
  const tekst = pole.value.trim();
  if (!tekst) return;
  dodaj(tekst);
  pole.value = '';
  pole.focus();
});

// 4. start
renderuj();
Podgląd

Kompletny plik do zapisania na dysku (ten sam JS, bez komunikatu o trybie):

zadania.html
<!DOCTYPE html>
<html lang="pl">
<head>
  <meta charset="UTF-8">
  <title>Lista zadań</title>
  <style>
    body { font-family: system-ui, sans-serif; max-width: 480px; margin: 40px auto; }
    li { display: flex; gap: 8px; align-items: center; padding: 4px 0; }
    li.zrobione span { text-decoration: line-through; color: #888; }
  </style>
</head>
<body>
  <h1>Lista zadań</h1>
  <form id="formularz">
    <input id="nowe" placeholder="Nowe zadanie" autocomplete="off" required>
    <button type="submit">Dodaj</button>
  </form>
  <ul id="lista"></ul>
  <p id="podsumowanie"></p>

  <script>
    const KLUCZ = 'zadania';

    function odczytaj(klucz, domyslna) {
      try {
        const tekst = localStorage.getItem(klucz);
        return tekst === null ? domyslna : JSON.parse(tekst);
      } catch (e) {
        console.error('Odczyt nieudany:', e.name);
        return domyslna;
      }
    }
    function zapisz(klucz, wartosc) {
      try {
        localStorage.setItem(klucz, JSON.stringify(wartosc));
      } catch (e) {
        console.error('Zapis nieudany:', e.name);
      }
    }

    let zadania = odczytaj(KLUCZ, []);
    const lista = document.getElementById('lista');

    function renderuj() {
      lista.innerHTML = '';
      for (const z of zadania) {
        const li = document.createElement('li');
        li.className = z.zrobione ? 'zrobione' : '';
        const tekst = document.createElement('span');
        tekst.textContent = z.tekst;
        const wykonane = document.createElement('button');
        wykonane.textContent = z.zrobione ? 'Cofnij' : 'Wykonane';
        wykonane.addEventListener('click', () => przelacz(z.id));
        const usun = document.createElement('button');
        usun.textContent = 'Usuń';
        usun.addEventListener('click', () => usunZadanie(z.id));
        li.append(tekst, wykonane, usun);
        lista.append(li);
      }
      const zrobione = zadania.filter((z) => z.zrobione).length;
      document.getElementById('podsumowanie').textContent = `Zadań: ${zadania.length}, wykonanych: ${zrobione}`;
    }

    function aktualizuj(nowyStan) {
      zadania = nowyStan;
      zapisz(KLUCZ, zadania);
      renderuj();
    }
    function dodaj(tekst) {
      aktualizuj([...zadania, { id: Date.now(), tekst, zrobione: false }]);
    }
    function przelacz(id) {
      aktualizuj(zadania.map((z) => (z.id === id ? { ...z, zrobione: !z.zrobione } : z)));
    }
    function usunZadanie(id) {
      aktualizuj(zadania.filter((z) => z.id !== id));
    }

    document.getElementById('formularz').addEventListener('submit', (event) => {
      event.preventDefault();
      const pole = document.getElementById('nowe');
      const tekst = pole.value.trim();
      if (!tekst) return;
      dodaj(tekst);
      pole.value = '';
      pole.focus();
    });

    renderuj();
  </script>
</body>
</html>

Test powtórkowy

Test powtórkowy

Błędy, JSON i localStorage: 10 pytań.

0/0poprawnych
Sprawdź się

Co wypisze ten kod?

try {
console.log('1');
null.x;
console.log('2');
} catch (e) {
console.log('3');
} finally {
console.log('4');
}
Sprawdź się

Co wypisze ten kod?

const dane = JSON.parse('{"a":1}');
console.log(dane.a);
Sprawdź się

Co wypisze ten kod?

const o = { a: undefined, b: () => 1, c: NaN, d: 'x' };
console.log(JSON.stringify(o));
Sprawdź się

Które wywołanie JSON.parse NIE rzuci błędu?

Sprawdź się

Co wypisze ten kod w przeglądarce?

localStorage.setItem('n', 7);
console.log(localStorage.getItem('n') + 1);
Sprawdź się

Co zwraca localStorage.getItem('klucz'), gdy takiego klucza nie ma?

Sprawdź się

Jak poprawnie zapisać tablicę obiektów zadania w localStorage?

Sprawdź się

Które zdanie o throw jest prawdziwe?

Sprawdź się

Co wypisze ten kod?

class BladDanych extends Error {
constructor(m) { super(m); this.name = 'BladDanych'; }
}
try {
throw new BladDanych('zły format');
} catch (e) {
console.log(e instanceof Error, e.name, e.message);
}
Sprawdź się

Czym różni się sessionStorage od localStorage?