localStorage y sessionStorage en JavaScript: persistencia en el navegador

localStorage y sessionStorage son las dos formas de almacenar datos en el navegador del usuario sin necesidad de servidor. Son simples, sincrónicas y bien soportadas, pero tienen limitaciones importantes que hay que conocer antes de usarlas en producción.

localStorage vs sessionStorage: la diferencia clave

Ambas APIs tienen exactamente la misma interfaz. La diferencia es el ciclo de vida de los datos:

  • localStorage: persiste entre sesiones. Los datos sobreviven al cierre del navegador y se comparten entre pestañas del mismo origen.
  • sessionStorage: dura solo mientras la pestaña está abierta. Se crea nueva por pestaña, incluso del mismo origen, y no se comparte entre ellas.
// setItem y getItem
localStorage.setItem('tema', 'oscuro');
const tema = localStorage.getItem('tema');
console.log(tema);  // 'oscuro'

// Valor inexistente devuelve null (no undefined)
console.log(localStorage.getItem('inexistente'));  // null

// Eliminar y limpiar
localStorage.removeItem('tema');
localStorage.clear();  // Elimina todo del localStorage de este origen

// sessionStorage: misma API
sessionStorage.setItem('token_temporal', 'abc123');

Almacenar objetos con JSON.stringify

localStorage solo almacena strings. Para guardar objetos o arrays hay que serializarlos:

// Guardar objeto
const usuario = { id: 42, nombre: 'Ana', rol: 'admin' };
localStorage.setItem('usuario', JSON.stringify(usuario));

// Recuperar objeto
const guardado = localStorage.getItem('usuario');
const usuarioRecuperado = guardado ? JSON.parse(guardado) : null;
console.log(usuarioRecuperado?.nombre);  // 'Ana'

// Patrón completo con manejo de errores:
function guardarEnStorage(clave, valor) {
  try {
    localStorage.setItem(clave, JSON.stringify(valor));
  } catch (err) {
    // QuotaExceededError: el storage está lleno
    console.error('Error al guardar en localStorage:', err);
  }
}

function obtenerDeStorage(clave, valorPorDefecto = null) {
  try {
    const item = localStorage.getItem(clave);
    return item !== null ? JSON.parse(item) : valorPorDefecto;
  } catch (err) {
    // JSON inválido u otro error
    console.error('Error al leer de localStorage:', err);
    return valorPorDefecto;
  }
}

// Uso:
guardarEnStorage('preferencias', { idioma: 'es', notificaciones: true });
const prefs = obtenerDeStorage('preferencias', { idioma: 'es' });

Detectar cambios con el evento storage

El evento storage permite que otras pestañas del mismo origen reaccionen a cambios en localStorage (pero NO en sessionStorage, que es por pestaña):

// Esta pestaña escucha cambios hechos por OTRAS pestañas
window.addEventListener('storage', (evento) => {
  console.log('Clave cambiada:', evento.key);
  console.log('Valor anterior:', evento.oldValue);
  console.log('Valor nuevo:', evento.newValue);
  console.log('URL de la pestaña que cambió:', evento.url);

  if (evento.key === 'sesion' && evento.newValue === null) {
    // Otra pestaña cerró la sesión ? cerrar aquí también
    redirigirALogin();
  }
});

Cuándo preferir cookies

localStorage no es siempre la mejor opción. Las cookies tienen funcionalidades que localStorage no tiene:

// localStorage:
// ? Más almacenamiento (5-10MB vs 4KB de cookies)
// ? API más simple
// ? No se envía al servidor automáticamente
// ? No tiene expiración automática
// ? Accesible desde JavaScript (XSS puede leerlo)

// Cookies (con atributos de seguridad):
// ? Se envían al servidor en cada petición HTTP
// ? HttpOnly: inaccesible desde JavaScript (protege de XSS)
// ? SameSite: protege de CSRF
// ? Secure: solo HTTPS
// ? Expiration: caducan automáticamente

// Para tokens de autenticación, las cookies HttpOnly son más seguras:
// document.cookie = "token=abc; HttpOnly; Secure; SameSite=Strict"
// (aunque HttpOnly solo puede setearlo el servidor, no el JS)

// localStorage es ideal para:
// - Preferencias de usuario (tema, idioma)
// - Datos de la UI que no son sensibles
// - Caché de datos públicos
// - Estado de la aplicación (carrito, filtros activos)

Errores frecuentes: SSR y modo incógnito

// ERROR 1: acceder a localStorage en SSR (Next.js, Nuxt...)
// El código se ejecuta en Node.js donde window no existe

// MAL:
const tema = localStorage.getItem('tema');  // ReferenceError en SSR

// BIEN: comprobar que window existe
const tema = typeof window !== 'undefined'
  ? localStorage.getItem('tema')
  : 'claro';

// O usar useEffect en React (solo se ejecuta en el cliente):
useEffect(() => {
  const temaGuardado = localStorage.getItem('tema');
  if (temaGuardado) setTema(temaGuardado);
}, []);

// ERROR 2: modo incógnito con storage lleno
// En algunos navegadores en modo incógnito, localStorage puede
// lanzar QuotaExceededError incluso antes de guardar nada
// La solución es siempre envolver en try/catch (como se mostró arriba)

// Verificar disponibilidad:
function storageDisponible(tipo) {
  try {
    const storage = window[tipo];
    const test = '__test__';
    storage.setItem(test, test);
    storage.removeItem(test);
    return true;
  } catch {
    return false;
  }
}

const hayLocalStorage = storageDisponible('localStorage');

La capacidad típica de localStorage es de 5MB por origen en la mayoría de navegadores. No está diseñado para guardar cantidades grandes de datos: para eso existen IndexedDB (para bases de datos en el cliente) o la Cache API (para assets de Service Workers).

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP