Chrome Extension alarms often look harmless: give a scheduled task a name, choose a time, and handle the event later. Chrome 150 adds an important boundary that QA teams should test explicitly. Google says chrome.alarms.create() throws a TypeError when the alarm name is longer than 1024 bytes. Chrome 148 and 149 warned about the same condition before it became an error.
The word bytes is the detail that turns this into a useful test-design problem. JavaScript string length is not a reliable UTF-8 byte count. A name that looks shorter than 1024 characters can still cross the limit when it contains accented characters, CJK text, emoji, combining marks, or user-supplied data. This tutorial builds a small boundary harness, separates identifiers from payloads, and verifies recovery in a real Manifest V3 extension.
Why the Chrome 150 alarm limit matters to QA
Most extensions use compact constants such as sync or daily-reminder, so they will never approach the boundary. Risk appears when an implementation treats the alarm name as a storage field. Examples include serializing a URL, account identifier, JSON payload, search query, localization text, or a composite key into the name. The official announcement recommends moving intentional data out of alarm names and into chrome.storage.
A failure can be easy to miss. The extension may still install, its popup may still open, and short test data may pass. Only a production-shaped identifier or localized value triggers the creation error. If the caller does not catch it, the user sees a reminder that was apparently saved but never fires. Your release gate therefore needs both API evidence and a visible reminder outcome.
Freeze the test identity before running the matrix
Record the Chrome channel and full build, operating system, locale, extension version and ID, manifest hash, alarm-name construction function hash, input-normalization policy, storage schema version, service-worker state, and test-data revision. Keep each execution tied to an evidence ID. This prevents a Canary result, a Stable result, and a modified fixture from being combined into one misleading conclusion.
Run at least three browser lanes: Chrome 148 or 149 to observe the warning-era behavior where available, Chrome 150 to verify the thrown error, and your current supported Chrome build to protect the forward path. If your product declares a minimum Chrome version, test installation and upgrade behavior around that contract rather than silently assuming every user has already moved.
Step 1: inventory every alarm creation path
Search the service worker and shared utilities for every call to alarms.create. Trace where each name originates. Include upgrade migrations, retry helpers, import flows, server-driven schedules, and code paths behind feature flags. For every call, classify the name as a fixed identifier, a bounded generated identifier, or an unbounded value.
| Name source | Example risk | QA action |
|---|---|---|
| Fixed constant | Low, but may collide | Verify uniqueness and replacement semantics |
| Composite identifier | Hidden growth as fields expand | Test every component and delimiter |
| User or server text | Unicode and unbounded input | Remove payload from the name |
| Serialized JSON or URL | High byte count and data leakage | Migrate to storage with an opaque key |
Do not log the complete legacy value merely to prove that it is long. Record the fixture ID, character count, UTF-8 byte count, result category, error name, and a one-way digest. Synthetic inputs are safer and more reproducible than copied production identifiers.
Step 2: measure UTF-8 bytes correctly
Use TextEncoder in the extension runtime. Keep character count only as diagnostic context; make the byte count the boundary oracle.
const utf8Bytes = value => new TextEncoder().encode(value).byteLength; const evidence = name => ({ fixtureId: 'alarm-boundary-v1', characters: name.length, bytes: utf8Bytes(name), digestRequired: true });
Build deterministic fixtures for exactly 1023, 1024, and 1025 bytes. Do not create them by guessing how many emoji fit. Generate candidates, measure them with the same UTF-8 definition, and assert the measured size before invoking the API. Include ASCII, an accented sequence, CJK characters, emoji, and combining forms. Test normalized and non-normalized equivalents as separate fixtures because visual similarity does not imply identical bytes.
Also cover empty and omitted names, duplicate compact names, case variants, leading and trailing whitespace, malformed input produced by an unsafe decoder, and very large values. The goal is not only to pass the documented edge; it is to expose every place where the product confuses display text, payload data, and a stable scheduling identifier.
Step 3: make creation failures observable
Wrap alarm creation at one boundary so the UI receives a clear, non-sensitive result. On Chrome 150, a value above 1024 bytes should be rejected without leaving the product in a false-success state.
async function createReminder(name, when) { const bytes = new TextEncoder().encode(name).byteLength; if (bytes > 1024) return { ok: false, code: 'ALARM_NAME_TOO_LARGE', bytes }; try { await chrome.alarms.create(name, { when }); return { ok: true, bytes }; } catch (error) { return { ok: false, code: error?.name || 'CREATE_FAILED', bytes }; } }
This guard improves the user experience, but it is not a substitute for browser-matrix testing. Assert the native Chrome 150 behavior in a focused compatibility test, then assert your wrapper’s stable product contract. Verify that a rejected request does not create storage residue, success telemetry, a toast saying scheduled, or a partially rendered reminder.
Step 4: replace payload-bearing names with opaque IDs
A durable design keeps the alarm name small and stores structured details separately. Generate a compact opaque ID, use reminder:<id> as the alarm name, and save the reminder object under the same ID in chrome.storage.local. The onAlarm handler extracts the ID, loads the record, validates its schema, performs the intended action, and records completion.
async function scheduleReminder(reminder) { const id = crypto.randomUUID(); const alarmName = `reminder:${id}`; await chrome.storage.local.set({ [`reminder:${id}`]: reminder }); try { await chrome.alarms.create(alarmName, { when: reminder.when }); } catch (error) { await chrome.storage.local.remove(`reminder:${id}`); throw error; } return id; }
Test the transaction in both directions. If storage succeeds and alarm creation fails, the record must be removed or marked recoverable. If storage fails, alarm creation must not proceed. If the worker terminates between operations, startup reconciliation must repair or quarantine the incomplete state. Use a schema version so future migrations can identify which records are safe to convert.
Step 5: test legacy migration as an idempotent workflow
Create a disposable extension version that represents the old format and populate synthetic legacy alarms. Upgrade to the fixed version. The migration should list known alarms, transform only identifiers owned by the extension, copy payload data into storage, create the compact replacement, verify it, and clear the legacy alarm. Mark each completed record so repeating the migration does not duplicate reminders.
Exercise interruption after every meaningful step: before storage, after storage, after replacement creation, before legacy deletion, and after deletion but before the migration marker. Restart Chrome and terminate the service worker between attempts. The official lifecycle guide warns that service workers can be terminated after inactivity and recommends persisting data instead of relying on globals. A migration that works only while one worker instance remains alive is not release-ready.
Add negative cases for corrupted storage, missing payloads, a pre-existing replacement alarm, duplicate legacy identifiers, expired schedules, a clock change, storage quota failure, and rollback to the previous extension version. Define the rollback result before testing: preserve data, disable only the affected reminder, or require a user-visible recovery step. Never improvise that policy during an incident.
Step 6: automate exact boundaries in a real browser
Unit-test the encoder and name builder first, then load the packaged extension in a real Chrome build. Google’s end-to-end testing guide supports browser automation libraries including Puppeteer, Playwright, Selenium, and WebdriverIO and recommends assertions based on user-visible behavior.
- Open the extension test page or popup and schedule each synthetic fixture.
- Capture the visible confirmation state, measured byte count, and API result.
- Query the alarm inventory and confirm presence or absence by opaque ID.
- Advance to a near-future alarm and prove the visible reminder fires once.
- Restart the browser, repeat startup reconciliation, and confirm no duplicate delivery.
- Terminate the service worker through a realistic path and verify persisted recovery.
Be careful with drivers that keep extension service workers alive. The official testing guidance notes that some automation setups change normal termination behavior. Keep a dedicated lifecycle lane using a method that allows the worker to stop; do not call a continuously attached debugger test restart coverage.
Recommended boundary matrix
| Fixture | Expected at 1024 bytes | Expected above 1024 bytes |
|---|---|---|
| ASCII | Create succeeds | Chrome 150 rejects |
| Accented text | Create succeeds when bytes equal 1024 | Reject even if character count is lower |
| CJK or emoji | Same byte-based rule | Reject and show no false success |
| Combining sequence | Evaluate the exact stored sequence | Reject using encoded bytes |
| Opaque migrated ID | Remain far below the limit | Not applicable by design |
For Chrome 148 and 149, capture the documented warning behavior but avoid making console text your only oracle. For Chrome 150 and later, assert the failure category and the absence of a created alarm. In all lanes, verify product behavior: no lost reminder, no duplicate, no stale success state, and no sensitive data in logs.
Release gate for QA engineers and SDETs
- Every creation path is inventoried and classed as fixed, bounded, or migrated.
- The harness asserts UTF-8 byte counts at 1023, 1024, and 1025 bytes.
- Unicode, normalization, duplicate, concurrent, and malformed cases are covered.
- Chrome 148/149 warning-era and Chrome 150 error behavior are separated.
- Payloads live in structured storage behind compact opaque alarm IDs.
- Creation and storage failures cannot produce false success or orphaned state.
- Migration is resumable, idempotent, privacy-safe, and rollback-tested.
- Worker termination and full browser restart have independent coverage.
- Visible reminder delivery agrees with the alarm and storage evidence.
- A human reviews migration evidence and rollback readiness before release.
Final takeaway
The Chrome 150 change is a small platform rule with an excellent testing lesson: boundaries must use the platform’s unit, not the unit that is easiest to count. Measure UTF-8 bytes, keep identifiers compact, move payloads into storage, test the native error and your product response, and prove recovery across service-worker and browser lifecycles. That turns a possible silent reminder loss into a controlled, observable compatibility migration.

