---
title: 'Caveats and migration'
description: 'Understand storage limits, synchronization boundaries, invalid data, and migration behavior.'
---

Adding a storage wrapper does not remove browser storage limits or make existing data automatically compatible. Before adopting ultrastorage or changing its configuration, understand how it handles older entries, failed reads, expiration, and notifications. These details help you plan migrations and avoid unexpected behavior.

- **Same-page writes must go through ultrastorage to notify**: Writes made directly through `localStorage`, `sessionStorage`, or a custom `Storage` object do not emit same-page notifications. Cross-tab browser events are still observed. Coordination is limited to instances sharing one loaded library runtime; separately bundled copies and mixed ESM/CJS runtimes do not share a registry.
- **Use the React adapter for rendering**: `getItem()` returns fresh objects and can delete expired entries. `ultrastorage/react` supplies cached, side-effect-free snapshots internally; no public snapshot API is exposed.
- **Still synchronous storage**: This wraps `localStorage`-style APIs, so reads and writes are still synchronous and still subject to browser storage quotas.
- **Changing serializers can strand old entries**: `getItem()` has a JSON fallback, but enumeration-based APIs like `clear()`, `clearExpired()`, `key()`, and `length` depend on the current serializer being able to parse old values. If you ever need to change serializers, we recommend changing the `prefix` and using a new namespace.
- **Expired entries are cleaned up lazily**: Expired data is hidden from reads immediately, but it may still occupy storage until `getItem()` touches it or `clearExpired()` is called.
- **`null` means several things**: `getItem()` returns `null` for missing keys, expired entries, foreign values not written by `ultrastorage`, parse failures, and schema validation failures.
- **Stored `null` and missing keys look the same**: If that distinction matters, pair `getItem()` with `has()`.
- **Existing raw `localStorage` values are invisible**: This library only reads entries with its internal marker, so migrating older plain-string or plain-JSON data requires explicit migration code. You can use `getOrInit()` for this purpose, specifying a `factory` function that reads from the existing `localStorage` key.
- **No atomic updates across tabs or callers**: `getOrInit()` and `updateItem()` are read-modify-write helpers, not transactional operations.
- **Schema validation is sync-only and non-destructive**: Async schemas throw, and invalid stored values return `null` without being removed automatically.
