skills/source/errors/frappe-errors-clientscripts/SKILL.md
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.
npx skillsauth add OpenAEC-Foundation/ERPNext_Anthropic_Claude_Development_Skill_Package frappe-errors-clientscriptsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Cross-refs: frappe-syntax-clientscripts (syntax), frappe-impl-clientscripts (workflows), frappe-errors-serverscripts (server-side).
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 |
|---------------|-------|-----|
| 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 |
// ❌ 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.
// ❌ 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);
}
}
});
// ❌ 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
}
});
// ❌ 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');
}
}
});
// ❌ 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
}
}
});
// ❌ 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;
}
});
| Tool | How to Use | When |
|------|-----------|------|
| Browser Console (F12) | console.log(frm.doc) | Inspect form state |
| console.table() | console.table(frm.doc.items) | View child table rows |
| JSON.parse(JSON.stringify(frm.doc)) | Deep-clone for snapshot | Avoid circular refs in console |
| frappe.boot.developer_mode | Check if dev mode on | Conditional debug logging |
| frappe.ui.toolbar.clear_cache() | Clear client cache | After deploying script changes |
| Network tab (F12) | Filter XHR requests | Inspect frappe.call payloads |
| frappe.show_alert({message: 'debug', indicator: 'blue'}, 5) | Visual debug in UI | Quick feedback without console |
frm parameter — NEVER use cur_frm [v14+]__() for all user-facing strings — Required for translationfrappe.throw()frappe.get_doc(cdt, cdn) to access child table rows in eventsfrappe.throw() only in validate to prevent saver.message for null before accessing server response propertiesfrappe.model.set_value(cdt, cdn, field, value) in child table eventsalert(), confirm(), or prompt() — Use frappe.msgprint / frappe.confirmcur_frm — It is unreliable and undefined in many contextsconsole.log in production — Use conditional frappe.boot.developer_mode check.then() and await in the same function — Pick one patternfrm.set_value in setup — Form is not ready; use refresh or onloaderror callback on frappe.call when using callback style| File | Contents |
|------|----------|
| references/examples.md | Real error scenarios with diagnosis |
| references/anti-patterns.md | Common mistakes with before/after fixes |
| references/patterns.md | Defensive error handling patterns |
tools
Use when implementing OAuth providers, Connected Apps, Webhooks, Payment Gateways, or Data Import/Export in Frappe. Prevents authentication failures from wrong OAuth flow, missed webhook deliveries, and data corruption during bulk imports. Covers OAuth2 provider/client, Connected App DocType, Webhook DocType, Payment Gateway integration, Data Import, Data Export, frappe.integrations module. Keywords: OAuth, Connected App, Webhook, Payment Gateway, Data Import, Data Export, integration, API key, OAuth2, webhook trigger, connect to external service, OAuth setup, webhook configuration, import data, export data..
development
Use when implementing hooks.py configurations in a Frappe custom app. Covers step-by-step workflows for doc_events, scheduler_events, override/extend_doctype_class, permission hooks, extend_bootinfo, fixtures, asset injection, website hooks, and doctype_js. Prevents broken transactions, missed migrations, and multi-app conflicts. Keywords: hooks.py, doc_events, scheduler_events, override doctype,, how to add hook, when to use doc_events, scheduler setup, override existing behavior. extend doctype class, permission hook, scheduler job, fixtures, doctype_js, extend_bootinfo, website hooks.
development
Use when building a custom Frappe app from scratch. Covers bench new-app walkthrough, app structure decisions, adding DocTypes, hooks, patches, fixtures management, development workflow (bench migrate, build, clear-cache), testing, packaging, installing on another site, version management, and app dependencies for v14/v15/v16. Keywords: create custom app, new frappe app, bench new-app, app structure, module creation, doctype creation, fixtures, patches, deployment, packaging, data migration, patch file, patches.txt, migrate data between DocTypes, create new app from scratch.
development
Use when building Document Controllers in a custom Frappe app: file creation, lifecycle hooks, validation, autoname, submittable workflows, controller override, child table controllers, flags system, migration from hooks.py and Server Scripts. Keywords: how to implement controller, which hook to use, validate vs on_update, override controller, submittable document, autoname, flags, extend_doctype_class, controller testing, child table controller, which hook to use, when does validate run, how to override save, document lifecycle.