You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Use when debugging or preventing errors in Frappe Client Scripts. Prevents TypeError, frappe.call failures, async/await mistakes, cur_frm vs frm confusion, field not found, child table access errors, timing issues, CSRF token errors, and permission denied on frappe.call. Covers error diagnosis flowchart and debug tools for v14/v15/v16. Keywords: client script error, TypeError, frappe.call, async await,, Cannot read properties of undefined, TypeError, browser console error, script not running, form not updating. cur_frm, field not found, child table, CSRF, permission denied.
license
MIT
compatibility
Claude Code, Claude.ai Projects, Claude API. Frappe v14-v16.
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
TypeError: Cannot read properties of undefined (reading 'fieldname')
Field does not exist on DocType or doc not loaded
ALWAYS check frm.doc exists before accessing fields
TypeError: frm.set_value is not a function
Using cur_frm shortcut that is undefined
ALWAYS use the frm parameter from event handler
Uncaught (in promise)
Unhandled async rejection from frappe.call
ALWAYS wrap async calls in try/catch
CSRFTokenError / 403 with CSRF
Token mismatch after session timeout
ALWAYS use frappe.call() (handles CSRF automatically)
Not permitted / 403 on frappe.call
Server method missing @frappe.whitelist()
ALWAYS add @frappe.whitelist() decorator to API methods
frappe.throw() not preventing save
frappe.throw() used outside validate event
ALWAYS use frappe.throw() only in validate
field not found: xyz in set_query
Fieldname typo or field not in child table
Verify exact fieldname against DocType definition
row.item_code is undefined
Accessing child row wrong — locals not synced
Use frappe.get_doc(cdt, cdn) in child table events
frm.set_value not working
Called in setup before form fully loaded
Move field-setting logic to refresh event
Maximum call stack exceeded
Circular trigger — field change fires own handler
Use frm.flags guard to break recursion
Critical Error Patterns
1. cur_frm vs frm: The #1 Beginner Mistake
// ❌ WRONG — cur_frm is undefined in many contextsfrappe.ui.form.on('Sales Order',{customer(frm){cur_frm.set_value('territory','Default');// BREAKS in list view}});// ✅ CORRECT — ALWAYS use the frm parameterfrappe.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 silentlyfrappe.ui.form.on('Sales Order',{asynccustomer(frm){letr=awaitfrappe.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 checkfrappe.ui.form.on('Sales Order',{asynccustomer(frm){if(!frm.doc.customer)return;try{letr=awaitfrappe.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 statefrappe.ui.form.on('Sales Order Item',{item_code(frm,cdt,cdn){letrow=frm.doc.items.find(r=>r.name===cdn);// fragilerow.rate=100;// Does not trigger UI refresh}});// ✅ CORRECT — Use frappe.get_doc and frappe.model.set_valuefrappe.ui.form.on('Sales Order Item',{item_code(frm,cdt,cdn){letrow=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 readyfrappe.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/onloadfrappe.ui.form.on('Sales Order',{setup(frm){// Filters belong in setupfrm.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 savefrappe.ui.form.on('Sales Order',{customer(frm){if(!frm.doc.customer){frappe.throw(__('Customer required'));// Stops script, NOT save}}});// ✅ CORRECT — throw in validate prevents savefrappe.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 cyclefrappe.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;}});