Skip to main content

3 posts tagged with "JavaScript"

View all tags

Old Browser Extension Stops Syncing Mid-Month? Version Skew from a Re-purposed Sync Flag

Β· 5 min read

After the "crowd asset" collection feature of our browser extension shipped, support started hearing the same story: some users open crowd assets mid-month and see "already up to date" β€” while their data stops at the 1st. The un-upgraded old extension had stopped collecting, and would only resume on the first day of the next month.

Encountered this while building e-commerce data collection tooling β€” a one-stop e-commerce operations solution from data collection to smart analytics; the collection flag is served by the backend, and old and new extension versions share the same server-side table.

TL;DR​

The new release gave the existing boolean flag is_current_month a new meaning ("this month's collection window is covered"); un-upgraded clients read it with the old meaning ("data is current"), their idempotency check short-circuited, and collection stopped mid-month. This is not a bug to fix β€” it's version skew: one field, two interpretations. The impact has a natural self-healing boundary (the flag flips false next month); the response is upgrade prompts + idempotent upsert (no rollback), and the prevention is: semantic changes must come with a new field.

Symptoms​

New extension collects: writes the current-month row, is_current_month = true

Old extension syncs: reads that row β†’ hits the "up-to-date" check β†’ stops collecting
User's view: mid-month, "crowd assets" says already current; data frozen at month start
Next month, day 1: is_current_month flips false β†’ old version resumes (self-heals)

The eerie part: server data is perfectly correct, the new extension works, the old extension's code never changed β€” the only failure mode is "old version reading rows written by the new version".

Root Cause​

Textbook version skew. The flag's name stayed, its meaning moved: to the new release it means "current-month window covered"; the old release, following its own historical semantics, reads the same row as "data is current" β€” and its perfectly-correct idempotency check ("already current β†’ skip re-collection") short-circuits the whole collection. Both sides' logic is right; the wrong part is letting two semantics share one field.

Client (and every client-side) version distribution is outside the server's control: after a release, old and new versions coexist for weeks as the norm. Any change to a server-side field's meaning is read by every historical version β€” the same lesson as the classic feature-flag mistake: re-purpose an old flag to carry a new meaning, and readers act on the old meaning.

Solution​

Immediate response: prompt upgrades + idempotent writes​

When releasing the version with the new collection logic, explicitly ask users to upgrade β€” the old version will not recover on its own within the month. Server and data need no rollback: collection writes are idempotent upserts, so interleaved old/new writes produce no dirty data, and everything realigns when the flag flips next month.

Long-term prevention: new semantics, new field​

Move the "window semantics" off the boolean onto a new field with an explicit window; old clients that don't know the field can't misread it:

// anti-pattern: old flag re-purposed for new semantics
{ "is_current_month": true }

// correct: new semantics in a new field, explicit and comparable
{ "collected_window": "2026-08", "source_version": "2.3.0" }

The idempotency key decouples from business semantics: old versions judge by the old field, new versions by the new one, no cross-contamination.

Design principle: give every flag a self-healing boundary​

Prefer binding flags to natural time boundaries (month, day) rather than absolute semantics like "current". The month boundary here capped the worst case at one month; without such a boundary, skew is permanent and only a release can fix it.

Notes

  • You don't control the client version distribution. Treat any semantic change to a server-side field as a compatibility change: enumerate all readers and verify each interpretation path.
  • A self-healing boundary is a safety net, not a plan β€” "it'll be fine next month" is not an acceptable long-term state; make the business call explicitly.
  • Idempotent writes (upsert) are the precondition for a safe overlap period; collection pipelines without idempotency will produce duplicates or conflicts the moment versions coexist.
  • When debugging collection pipelines, isolate production data first β€” see adding a DRY-RUN mode to your Chrome extension collector.

FAQ​

What are the best practices for feature flags?​

One flag, one meaning. New semantics get a new field, not a re-purposed old flag. Bind flags to explicit time windows or versions. Before changing any meaning, enumerate every reader β€” especially un-upgraded clients β€” and confirm they won't interpret new values with old semantics. The mid-month collection stop here is the direct consequence of re-using an old flag.

How do I keep server-side fields backward compatible?​

Add, never mutate: existing fields keep their meaning, type, and behavior; new semantics go into new fields; old clients ignore what they don't recognize. Keeping a field's name while changing its meaning is a breaking change to every existing reader.

What should old clients do when they read data with new semantics?​

Three steps: bound the impact to a self-healing window (a calendar-month flip restores collection automatically); prompt upgrades at release to shorten the overlap window; keep server writes idempotent (upsert) so interleaved old/new writes blend safely β€” no rollback, no data cleanup.

CCLEE

Independent developer, 24 years in e-commerce, focused on grounding AI in real business scenarios.

Work with me

JavaScript throw; is a SyntaxError? JS has no bare rethrow β€” you must throw e

Β· 5 min read

Wanting to "just pass the exception up unchanged" from a catch block, I reflexively wrote throw; β€” the bare rethrow I was used to in C# β€” and tsx/esbuild immediately failed to transform it: Unexpected ";".

Encountered this while building AI Analytics β€” an LLM-powered analytics pipeline that surfaces market trends, user behavior, and sales data for precise operations.

TL;DR​

JavaScript has no bare rethrow syntax. throw; (a bare throw) is a compile-time SyntaxError in all three toolchains: Node, tsc, and esbuild. To rethrow a caught exception you must throw e (the catch block needs a binding); to throw a new one, throw new Error(...).

The symptom​

The same throw; produces differently-worded errors across toolchains, but all of them are syntax errors (not runtime errors):

try {
something();
} catch {
throw; // ← bare rethrow
}
ToolchainError
tsx / esbuildERROR: Unexpected ";" (transform fails)
Node.js native (.js / .mjs)SyntaxError: Unexpected token ';'
TypeScript compiler (tsc)error TS1109: Expression expected.

The misleading one is esbuild's Unexpected ";" β€” it's tempting to read as "esbuild/tsx doesn't support some newer syntax." But run the same snippet through native Node and you get the identical SyntaxError. This isn't a tool limitation; the language itself has no such form.

Root cause​

The ECMAScript throw statement mandates an expression:

ThrowStatement : throw Expression ;

That is, throw must be followed by a value (throw err, throw new Error(), throw "fail") β€” the slot before the semicolon cannot be empty. JavaScript has no "bare throw = rethrow the current exception" semantics, which is the key difference from C# / Java / Python:

LanguageRethrow current exceptionNeeds caught variable
C#throw;no
Javathrow e;yes
Pythonraiseno
JavaScriptthrow e;yes

One common confusion: ES2019 added optional catch binding (catch {} may omit the parameter), but that is orthogonal to bare throw. Even with a binding present, throw; still errors β€”

try { f(); } catch (e) { throw; }   // still a SyntaxError; e is NOT auto-fed to throw

Confirmed in tsx as Unexpected ";". The expression after throw cannot be omitted; there are no exceptions.

The fix​

Pick the form that matches your intent:

// 1. Rethrow the original exception β€” the most common need
try {
doWork();
} catch (e) {
log(e);
throw e; // βœ… include e
}

// 2. Wrap in a new exception
try {
doWork();
} catch (e) {
throw new Error(`failed: ${e.message}`); // βœ… throw + expression
}

// 3. With ES2019 catch {} (no parameter), there is nothing to rethrow β€” throw new
try {
doWork();
} catch {
throw new Error("doWork failed"); // βœ… throw; here would be wrong
}

A minimal runnable repro and fix β€” run it directly with tsx:

function risky(): void {
throw new Error("origin");
}

function rethrowOptional(): void {
try {
risky();
} catch (e) { // ← must receive e
console.log("caught, rethrowing");
throw e; // ← not throw;
}
}

try {
rethrowOptional();
} catch (e) {
console.log("recovered:", (e as Error).message); // origin
}

On the call stack: throw e reuses the same error object, whose .stack was captured at new Error time; rethrow does not overwrite it. Only throw new Error(...) generates a fresh stack from the current throw site. So "does rethrow lose the stack?" β€” no, as long as you don't construct a new error.

Another common exception-handling pitfall is a catch block that swallows the error entirely, surfacing as a silent failure β€” see Python task marked failed but no error? try/except swallowed it. Worth watching across every language.

Caveats​

Caveats

  • Optional catch binding is not the culprit: catch {} (ES2019) is legal on its own; the only problem is throw;. Don't add a parameter to catch just to "fix throw" unless you actually use the variable.
  • Same rule in async/await: try { await f() } catch (e) { throw; } is a SyntaxError inside async functions too β€” the rule doesn't distinguish sync from async.
  • Stack preservation: throw e keeps the original stack; throw new Error(...) refreshes it. Use the former when debugging and you need the earliest throw site.
  • Aligning cross-language habits: coming from C#/Python to JS, porting throw; / raise directly will always bite you; flag this pattern in code review.

FAQ​

How do you rethrow a caught exception in JavaScript?​

Use throw e, and catch must take a binding: catch (e) { ...; throw e; }. JavaScript has no bare rethrow β€” a standalone throw; is a SyntaxError that Node, tsc, and esbuild all reject at compile time. It is not a limitation of any single tool.

Does rethrowing an exception in JavaScript preserve the original stack?​

Yes. throw e reuses the same error object, whose .stack was captured when the error was constructed with new Error; rethrow neither overwrites nor resets it. Only throw new Error(...) generates a fresh stack from the current throw site β€” so if you want the earliest origin during debugging, use throw e.

How do you correctly rethrow inside a JavaScript try/catch?​

catch must receive the error and throw it back: try { ... } catch (e) { log(e); throw e; }. With ES2019's catch {} (parameter omitted) there is no variable to throw, so you can only throw new Error(...). Either way, throw must be followed by an expression β€” throw; is always illegal.

CCLEE

Independent developer, 24 years in e-commerce, focused on grounding AI in real business scenarios.

Work with me

Chrome Extension Collecting Empty Data? Same-Site Cookies with Different Formats Break ID Mapping

Β· 3 min read

Encountered this issue while building an e-commerce data collection system for a client. Here's the root cause and solution.

TL;DR​

The 1688 platform sets two cookies (last_mid and unb) both containing user IDs, but in different formats (b2b-xxx vs plain digits). The database stores the b2b- prefix format. The original code iterated the cookie array in the outer loop, matching unb first β€” the strict equality check failed, and all collected data was written with zero values because it couldn't map to a shop.

Fix: Put the cookie key priority list in the outer loop and the cookie array in the inner loop, ensuring high-priority keys are checked first.

Problem​

After collecting daily shop reports from 1688, all dashboard metrics in the database were 0, while inquiry data had values:

shop_id | report_date | reveal_cnt | uv | pay_amt | effective_inq_users
2 | 2026-05-13 | 0 | 0 | 0.00 | 56
2 | 2026-05-14 | 0 | 0 | 0.00 | 41

Server log: No shop found for memberId: 2214126315258

But the database stored platform_account_id as b2b-2214126315258ad300.

Root Cause​

Two cookies with different formats on the same domain​

unb=2214126315258                          # plain digits
last_mid=b2b-2214126315258ad300 # b2b- prefix + suffix

The shops.platform_account_id column stores the b2b- prefix format. The code used strict equality:

const match = mapping.find(m => m.platform_account_id === memberId);
// "2214126315258" !== "b2b-2214126315258ad300" β†’ no match

Iteration order trap​

The original code had the cookie array in the outer loop and the key list in the inner loop:

// ❌ Wrong: cookie array outer, key priority ineffective
var keys = ['last_mid', '__last_memberid__', 'unb'];
for (var i = 0; i < cookies.length; i++) { // outer: cookies
var pair = cookies[i].trim();
for (var k = 0; k < keys.length; k++) { // inner: keys
if (pair.indexOf(keys[k] + '=') === 0) {
return pair.substring(keys[k].length + 1);
}
}
}

document.cookie order is not guaranteed. If unb appears before last_mid in the array, it gets matched first β€” returning the plain-digit format and making last_mid priority useless.

Solution​

Swap loop levels: key priority list in the outer loop, cookie array in the inner loop:

// βœ… Correct: key priority list in outer loop
var keys = ['last_mid', '__last_memberid__', 'unb'];
for (var k = 0; k < keys.length; k++) { // outer: iterate keys by priority
for (var i = 0; i < cookies.length; i++) { // inner: search all cookies
var pair = cookies[i].trim();
if (pair.indexOf(keys[k] + '=') === 0) {
return pair.substring(keys[k].length + 1);
}
}
}

The key list is iterated by priority in the outer loop, so last_mid is always checked first regardless of document.cookie order, guaranteeing the user ID format matches the database.

Verification​

// Confirm both cookies exist in browser console
document.cookie.split(';')
.filter(c => /last_mid|unb/.test(c.trim()))
.map(c => c.trim())
// ['unb=2214126315258', 'last_mid=b2b-2214126315258ad300']

Note

This issue doesn't affect chrome.cookies.get() in extension pages β€” it queries by name directly. But when parsing document.cookie strings, always pay attention to loop levels. Also, if your extension passes data via postMessage, watch out for the targetOrigin wildcard security risk; and if messages are processed twice after hot reload, you'll need to manually manage listener lifecycle.