8.4 KiB
Debugging Firebase → PocketBase Migration Bugs
Common bug patterns discovered during migration validation, with root cause analysis and fix strategies.
1. Dual Render Path Inconsistency
After completing a repair order, it remains visible in the Active tab despite workStatus being set to 'completed'.
Root Cause
The app has two render paths that apply different filters:
loadRepairOrders() renderActiveRepairOrders()
│ │
│ filter: workStatus !== │ NO FILTER — passes entire
│ 'completed' && status !== │ repairOrders array to
│ 'completed' │ renderRepairOrders()
│ │
▼ ▼
renderRepairOrders(filtered) renderRepairOrders(all)
loadRepairOrders()(called on page load) fetches from DB, then filters out completed orders at lines ~2276-2278renderActiveRepairOrders()(called after every UI mutation) renders the localrepairOrdersarray without any filter
When handleFinancialCompletion() sets workStatus = 'completed' and calls renderActiveRepairOrders(), the completed RO stays visible because the re-render path doesn't filter.
Fix
function renderActiveRepairOrders() {
const activeOrders = repairOrders.filter(ro => {
return ro.workStatus !== 'completed' && ro.status !== 'completed';
});
renderRepairOrders(activeOrders);
}
This uses the same filter predicate as loadRepairOrders().
How to detect
Search for all functions that render the same list. The app typically has:
- A load function (fetches from DB, applies filters, assigns to global
repairOrders, calls render) - A render function (re-renders from the global array, used for reactive UI updates)
If the load function filters but the render function doesn't, this bug exists. To verify:
grep -n "function renderActiveRepairOrders\|function loadRepairOrders\|function renderRepairOrders" *.js
Then check:
- Does
loadRepairOrders()filter out completed items before callingrenderRepairOrders()? - Does
renderActiveRepairOrders()pass the raw array or a filtered one?
Search pattern
grep -n "function.*render.*Orders\|function.*load.*Orders\|\.filter.*completed" repair-orders.js
2. Silent Errors in setTimeout / Asynchronous Callbacks
A DOM element is missing from the HTML, causing a TypeError that crashes silently. The UI appears to "do nothing" when a button is clicked, with no console error visible.
Root Cause
// Outer try/catch:
try {
// ... sync code ...
setTimeout(() => {
// This code runs AFTER the try/catch scope has exited
document.getElementById('missing-element').textContent = value;
// TypeError here is UNCAUGHT — it fires in the event loop tick
// after the try/catch already returned
}, 100);
return; // <-- try/catch scope exits
} catch (error) {
// Never reaches here for the setTimeout error
console.error('Error:', error);
}
The try/catch only protects the code inside its lexical scope. setTimeout (and Promise.then, requestAnimationFrame, setInterval) execute in a new execution context — the outer try/catch is already gone.
How to find
- Look for DOM operations inside
setTimeout(),requestAnimationFrame(), or.then()callbacks - Check if they're inside a
try/catch— if thetry/catchwraps thesetTimeoutcall (not the callback body), the callback is unprotected - The ONLY way to catch these is a
try/catchinside the callback, orwindow.onerror/window.addEventListener('unhandledrejection', ...)
Fix
// Option A: try/catch INSIDE the callback
setTimeout(() => {
try {
document.getElementById('missing-element').textContent = value;
} catch (innerError) {
console.error('setTimeout error:', innerError);
}
}, 100);
// Option B: Replace setTimeout with synchronous code when possible
// (Only if the delay isn't needed for DOM readiness)
Detection script
Run this in browser DevTools to detect pattern:
// Check all JS files for setTimeout with DOM ops outside try/catch
document.querySelectorAll('script[src]').forEach(s => {
fetch(s.src).then(r => r.text()).then(code => {
const matches = code.match(/setTimeout\([^)]*getElementById[^)]*\)/g);
if (matches) console.log(`${s.src}: ${matches.length} vulnerable setTimeout(s)`);
});
});
3. "Looks like it works but data is missing" — PocketBase Silently Drops Undefined Fields
After a successful API call (HTTP 200), the saved record is missing fields the app wrote.
Root Cause
PocketBase is strictly typed — fields not defined in the collection schema are silently dropped on write. Firestore is schemaless, so any field in the JavaScript object gets stored.
How to find missing fields systematically
# 1. Get the PocketBase collection schema field names
PB_FIELDS=$(curl -s http://127.0.0.1:8091/api/collections/repairOrders | \
python3 -c "import sys,json; print('\n'.join(f['name'] for f in json.load(sys.stdin).get('fields',[])))")
# 2. Get all field names the JavaScript app writes (from CRUD operations)
JS_FIELDS=$(grep -ohP '\b(customerName|workStatus|roNumber|vehicleInfo|vin|mileage|services|estimatedTime|promisedTime|lastModified|completedTime|financial|writeupTime|status|userId|customerPhone|deviceModel|deviceType|issue|priority|technician|notes|price|customerId|quoteId|invoiceId|lastActivity|createdAt|updatedAt)\b' repair-orders.js | sort -u)
# 3. Compare — fields in JS that are NOT in PB schema
echo "Missing from PB schema:"
for f in $JS_FIELDS; do
echo "$PB_FIELDS" | grep -q "$f" || echo " - $f"
done
Commonly missed fields in a shop management app migration:
| Field | Type | Where It's Written |
|---|---|---|
workStatus |
text | updateRepairOrderWorkStatus(), createNewRepairOrder() |
financial |
json | handleFinancialCompletion() — nested object with cpTotal, cpCost, etc. |
roNumber |
text | createNewRepairOrder() — auto-generated RO-XXXXXXX |
vehicleInfo |
text | Repair order create form & customer data |
vin |
text | Repair order create form |
mileage |
text | Repair order create form |
services |
text | Repair order services field (comma-separated) |
estimatedTime |
text | Repair order promised time calculation |
promisedTime |
text | Created from estimatedTime + currentTime |
lastModified |
text | Updated on every mutation |
completedTime |
text | Set when workStatus becomes 'completed' |
Symptom: "Completing an RO doesn't stick"
If the user reports: "I marked it complete, it showed completed, but after refresh it's back in Active" — this is ALWAYS a schema mismatch. The app updates the local JavaScript array (shows correct UI), but the PocketBase write silently drops workStatus and financial because those fields aren't in the collection schema. On page reload, loadRepairOrders() fetches from PB and the RO has its original workStatus (missing = undefined), so it passes the "not completed" filter.
This is subtly different from the Dual Render Path bug (which happens BEFORE refresh — RO stays in Active tab immediately). The schema mismatch bug: RO correctly disappears from Active tab after completion, but reappears on next page load.
How to find
4. Reference Build Comparison Technique
When a Firebase build works but the PocketBase port doesn't, compare the same function in both:
# Find where functions live in both builds
grep -n "updateRepairOrderWorkStatus\|handleFinancialCompletion" \
/path/to/firebase/repair-orders.js \
/path/to/pocketbase/repair-orders.js
# Read and diff
diff <(sed -n '1747,1823p' /path/to/firebase/repair-orders.js) \
<(sed -n '1747,1823p' /path/to/pocketbase/repair-orders.js)
If the functions are identical, the bug is not in the PocketBase adapter — it's a pre-existing bug in the app code that was hidden by Firebase behavior (realtime listeners auto-fixing the UI, different execution timing, etc.). In this case, the Firebase build had the same renderActiveRepairOrders filter bug, but it may have appeared to work because:
- Users refreshed the page (which calls
loadRepairOrders()with the filter) - The issue existed but was never noticed because the financial completion was never tested end-to-end