Frappe_Claude_Skill_Package frappe-errors-clientscripts
install
source · Clone the upstream repo
git clone https://github.com/OpenAEC-Foundation/Frappe_Claude_Skill_Package
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/OpenAEC-Foundation/Frappe_Claude_Skill_Package "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/source/errors/frappe-errors-clientscripts" ~/.claude/skills/openaec-foundation-frappe-claude-skill-package-frappe-errors-clientscripts && rm -rf "$T"
manifest:
skills/source/errors/frappe-errors-clientscripts/SKILL.mdsource content
Client Script Errors — Diagnosis and Resolution
Cross-refs:
frappe-syntax-clientscripts (syntax), frappe-impl-clientscripts (workflows), frappe-errors-serverscripts (server-side).
Error Diagnosis Flowchart
ERROR IN CLIENT SCRIPT │ ├─► TypeError: Cannot read properties of undefined │ ├─► "frm.doc.fieldname" → Field does not exist on DocType │ ├─► "r.message.value" → Server returned null/error │ └─► "row.fieldname" in child table → Row not fetched correctly │ ├─► frappe.call fails silently │ ├─► Missing error callback → Add error handler │ ├─► 403 Forbidden → Method not whitelisted (@frappe.whitelist) │ ├─► 417 Expectation Failed → Server-side frappe.throw() │ └─► 401 Unauthorized → Session expired or CSRF token invalid │ ├─► Uncaught (in promise) → Missing try/catch on async frappe.call │ ├─► Field appears blank after set_value → Timing issue (setup vs refresh) │ ├─► cur_frm is undefined → Using cur_frm in list/report context │ └─► frappe.throw() does not prevent save → Used outside validate event
Error Message → Cause → Fix Table
| Error Message | Cause | Fix |
|---|---|---|
| Field does not exist on DocType or doc not loaded | ALWAYS check exists before accessing fields |
| Using shortcut that is undefined | ALWAYS use the parameter from event handler |
| Unhandled async rejection from frappe.call | ALWAYS wrap async calls in try/catch |
/ | Token mismatch after session timeout | ALWAYS use (handles CSRF automatically) |
/ 403 on frappe.call | Server method missing | ALWAYS add decorator to API methods |
| used outside event | ALWAYS use only in |
in set_query | Fieldname typo or field not in child table | Verify exact fieldname against DocType definition |
| Accessing child row wrong — not synced | Use in child table events |
| Called in before form fully loaded | Move field-setting logic to event |
| Circular trigger — field change fires own handler | Use guard to break recursion |
Critical Error Patterns
1. cur_frm vs frm: The #1 Beginner Mistake
// ❌ WRONG — cur_frm is undefined in many contexts frappe.ui.form.on('Sales Order', { customer(frm) { cur_frm.set_value('territory', 'Default'); // BREAKS in list view } }); // ✅ CORRECT — ALWAYS use the frm parameter frappe.ui.form.on('Sales Order', { customer(frm) { frm.set_value('territory', 'Default'); } });
Rule: NEVER use
cur_frm. ALWAYS use the frm parameter passed to every event handler.
2. Async/Await: Silent Failure Without try/catch
// ❌ WRONG — Unhandled rejection crashes silently frappe.ui.form.on('Sales Order', { async customer(frm) { let r = await frappe.call({ method: 'myapp.api.get_data', args: { customer: frm.doc.customer } }); frm.set_value('credit_limit', r.message.limit); // r.message may be null } }); // ✅ CORRECT — try/catch with null check frappe.ui.form.on('Sales Order', { async customer(frm) { if (!frm.doc.customer) return; try { let r = await frappe.call({ method: 'myapp.api.get_data', args: { customer: frm.doc.customer } }); if (r.message) { frm.set_value('credit_limit', r.message.limit || 0); } } catch (error) { console.error('Customer fetch failed:', error); frappe.show_alert({ message: __('Could not load customer details'), indicator: 'red' }, 5); } } });
3. Child Table Access: Wrong Pattern
// ❌ WRONG — frm.doc.items[0] may not reflect latest state frappe.ui.form.on('Sales Order Item', { item_code(frm, cdt, cdn) { let row = frm.doc.items.find(r => r.name === cdn); // fragile row.rate = 100; // Does not trigger UI refresh } }); // ✅ CORRECT — Use frappe.get_doc and frappe.model.set_value frappe.ui.form.on('Sales Order Item', { item_code(frm, cdt, cdn) { let row = frappe.get_doc(cdt, cdn); if (!row.item_code) return; frappe.model.set_value(cdt, cdn, 'rate', 100); // Triggers refresh } });
4. Timing: setup vs refresh
// ❌ WRONG — set_value in setup, form not ready frappe.ui.form.on('Sales Order', { setup(frm) { frm.set_value('company', 'My Company'); // May not work } }); // ✅ CORRECT — set_query in setup, set_value in refresh/onload frappe.ui.form.on('Sales Order', { setup(frm) { // Filters belong in setup frm.set_query('customer', () => ({ filters: { disabled: 0 } })); }, refresh(frm) { // Value changes belong in refresh (or onload for new docs) if (frm.is_new()) { frm.set_value('company', 'My Company'); } } });
5. frappe.throw() Scope: Only Works in validate
// ❌ WRONG — throw in customer change does NOT prevent save frappe.ui.form.on('Sales Order', { customer(frm) { if (!frm.doc.customer) { frappe.throw(__('Customer required')); // Stops script, NOT save } } }); // ✅ CORRECT — throw in validate prevents save frappe.ui.form.on('Sales Order', { customer(frm) { if (!frm.doc.customer) { frappe.msgprint({ message: __('Customer required'), indicator: 'orange' }); } }, validate(frm) { if (!frm.doc.customer) { frappe.throw(__('Customer is required')); // Prevents save } } });
6. Recursion Guard with Flags
// ❌ WRONG — discount change triggers amount recalc, which triggers discount... frappe.ui.form.on('Sales Order', { discount_percent(frm) { frm.set_value('grand_total', calculate(frm)); // Fires on_change loop } }); // ✅ CORRECT — Use flags to break the cycle frappe.ui.form.on('Sales Order', { discount_percent(frm) { if (frm.flags.skip_recalc) return; frm.flags.skip_recalc = true; frm.set_value('grand_total', calculate(frm)); frm.flags.skip_recalc = false; } });
Debug Tools
| Tool | How to Use | When |
|---|---|---|
| Browser Console (F12) | | Inspect form state |
| | View child table rows |
| Deep-clone for snapshot | Avoid circular refs in console |
| Check if dev mode on | Conditional debug logging |
| Clear client cache | After deploying script changes |
| Network tab (F12) | Filter XHR requests | Inspect frappe.call payloads |
| Visual debug in UI | Quick feedback without console |
ALWAYS / NEVER Rules
ALWAYS
- Use the
parameter — NEVER usefrm
[v14+]cur_frm - Wrap async frappe.call in try/catch — Unhandled rejections fail silently
- Use
for all user-facing strings — Required for translation__() - Collect multiple validation errors before calling
frappe.throw() - Use
to access child table rows in eventsfrappe.get_doc(cdt, cdn) - Put
only infrappe.throw()
to prevent savevalidate - Check
for null before accessing server response propertiesr.message - Use
in child table eventsfrappe.model.set_value(cdt, cdn, field, value)
NEVER
- NEVER use
,alert()
, orconfirm()
— Use frappe.msgprint / frappe.confirmprompt() - NEVER expose stack traces to users — Log to console, show friendly message
- NEVER use
— It is unreliable and undefined in many contextscur_frm - NEVER leave
in production — Use conditionalconsole.log
checkfrappe.boot.developer_mode - NEVER mix
and.then()
in the same function — Pick one patternawait - NEVER call
infrm.set_value
— Form is not ready; usesetup
orrefreshonload - NEVER ignore the
callback onerror
when using callback stylefrappe.call
Reference Files
| File | Contents |
|---|---|
| Real error scenarios with diagnosis |
| Common mistakes with before/after fixes |
| Defensive error handling patterns |