
Frappe Errors Clientscripts
- 24 installs
- 159 repo stars
- Updated July 8, 2026
- openaec-foundation/frappe_claude_skill_package
Helps with ai & agent building tasks.
About
frappe-errors-clientscripts is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- frappe-errors-clientscripts
- AI & Agent Building
- AI-coding skill
Frappe Errors Clientscripts by the numbers
- 24 all-time installs (skills.sh)
- Ranked #9,876 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/openaec-foundation/frappe_claude_skill_package --skill frappe-errors-clientscriptsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 24 |
|---|---|
| repo stars | ★ 159 |
| Last updated | July 8, 2026 |
| Repository | openaec-foundation/frappe_claude_skill_package ↗ |
What it does
Helps with ai & agent building tasks.
Files
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 |
|---|---|---|
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 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) | 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 |
---
ALWAYS / NEVER Rules
ALWAYS
1. Use the `frm` parameter — NEVER use cur_frm [v14+] 2. Wrap async frappe.call in try/catch — Unhandled rejections fail silently 3. Use `__()` for all user-facing strings — Required for translation 4. Collect multiple validation errors before calling frappe.throw() 5. Use `frappe.get_doc(cdt, cdn)` to access child table rows in events 6. Put `frappe.throw()` only in `validate` to prevent save 7. Check `r.message` for null before accessing server response properties 8. Use `frappe.model.set_value(cdt, cdn, field, value)` in child table events
NEVER
1. NEVER use `alert()`, `confirm()`, or `prompt()` — Use frappe.msgprint / frappe.confirm 2. NEVER expose stack traces to users — Log to console, show friendly message 3. NEVER use `cur_frm` — It is unreliable and undefined in many contexts 4. NEVER leave `console.log` in production — Use conditional frappe.boot.developer_mode check 5. NEVER mix `.then()` and `await` in the same function — Pick one pattern 6. NEVER call `frm.set_value` in `setup` — Form is not ready; use refresh or onload 7. NEVER ignore the `error` callback on frappe.call when using callback style
---
Reference Files
| 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 |
Client Script Anti-Patterns — Error Prevention
Each anti-pattern shows the mistake, why it fails, and the correct approach.
---
1. Using cur_frm Instead of frm Parameter
// ❌ WRONG
frappe.ui.form.on('Sales Order', {
customer(frm) { cur_frm.set_value('territory', 'Default'); }
});
// ✅ CORRECT
frappe.ui.form.on('Sales Order', {
customer(frm) { frm.set_value('territory', 'Default'); }
});Why: cur_frm is undefined in list view, print, or when multiple tabs are open.
---
2. frappe.throw() Outside validate Event
// ❌ WRONG — Does NOT prevent save
frappe.ui.form.on('Sales Order', {
customer(frm) {
if (!frm.doc.customer) frappe.throw(__('Customer required'));
}
});
// ✅ CORRECT — Blocks save only in validate
frappe.ui.form.on('Sales Order', {
customer(frm) {
if (!frm.doc.customer) frappe.msgprint({ message: __('Select a customer'), indicator: 'orange' });
},
validate(frm) {
if (!frm.doc.customer) frappe.throw(__('Customer is required'));
}
});Why: frappe.throw() only prevents save when called within validate.
---
3. No try/catch on Async Calls
// ❌ WRONG — Silent failure
frappe.ui.form.on('Sales Order', {
async customer(frm) {
let r = await frappe.call({ method: 'myapp.api.get_data' });
frm.set_value('field', r.message.value);
}
});
// ✅ CORRECT
frappe.ui.form.on('Sales Order', {
async customer(frm) {
try {
let r = await frappe.call({ method: 'myapp.api.get_data' });
if (r.message) frm.set_value('field', r.message.value);
} catch (error) {
console.error('API failed:', error);
frappe.show_alert({ message: __('Could not load data'), indicator: 'red' }, 5);
}
}
});Why: Unhandled Promise rejections fail silently, confusing users.
---
4. Throwing on First Error (One at a Time)
// ❌ WRONG — User saves 5 times to find 5 errors
frappe.ui.form.on('Sales Order', {
validate(frm) {
if (!frm.doc.customer) frappe.throw(__('Customer required'));
if (!frm.doc.delivery_date) frappe.throw(__('Date required'));
if (!frm.doc.items?.length) frappe.throw(__('Items required'));
}
});
// ✅ CORRECT — All errors at once
frappe.ui.form.on('Sales Order', {
validate(frm) {
let errors = [];
if (!frm.doc.customer) errors.push(__('Customer is required'));
if (!frm.doc.delivery_date) errors.push(__('Delivery Date is required'));
if (!frm.doc.items?.length) errors.push(__('At least one item is required'));
if (errors.length) frappe.throw({ title: __('Please fix'), message: errors.join('<br>') });
}
});Why: Users should see all errors at once, not discover them one at a time.
---
5. Using Native alert() / confirm() / prompt()
// ❌ WRONG — Blocks thread, looks unprofessional
if (frm.doc.grand_total > 100000) {
if (!confirm('Large order. Continue?')) return false;
}
// ✅ CORRECT — Use frappe.confirm
frappe.confirm(
__('Large order ({0}). Continue?', [format_currency(frm.doc.grand_total)]),
() => { /* proceed */ },
() => { /* cancel */ }
);Why: Native dialogs block the thread, cannot be styled, and look outdated.
---
6. Exposing Technical Errors to Users
// ❌ WRONG
catch (error) { frappe.throw(error.stack); }
// ✅ CORRECT
catch (error) {
console.error('Technical error:', error);
frappe.msgprint({ title: __('Error'), message: __('An error occurred. Please try again.'), indicator: 'red' });
}Why: Stack traces confuse users and may expose sensitive internals.
---
7. No Null Check on Server Response
// ❌ WRONG — Crashes when r.message is null
callback(r) { frm.set_value('limit', r.message.credit_limit); }
// ✅ CORRECT
callback(r) { frm.set_value('limit', r.message?.credit_limit || 0); }Why: Server may return null on error, missing record, or permission failure.
---
8. Ignoring error Callback on frappe.call
// ❌ WRONG — No error handler
frappe.call({
method: 'myapp.api.process',
callback(r) { frappe.show_alert({ message: __('Done'), indicator: 'green' }); }
});
// ✅ CORRECT
frappe.call({
method: 'myapp.api.process',
callback(r) { if (r.message) frappe.show_alert({ message: __('Done'), indicator: 'green' }); },
error(r) {
console.error('Process failed:', r);
frappe.msgprint({ title: __('Error'), message: __('Failed. Please try again.'), indicator: 'red' });
}
});Why: Without error handler, failures are invisible to the user.
---
9. console.log in Production Code
// ❌ WRONG — Clutters production console
validate(frm) { console.log('customer:', frm.doc.customer); }
// ✅ CORRECT — Conditional debugging
const DEBUG = frappe.boot.developer_mode;
function debug(...args) { if (DEBUG) console.log('[MyApp]', ...args); }
validate(frm) { debug('customer:', frm.doc.customer); }Why: Production console logs may expose data and slow performance.
---
10. Mixing .then() and await
// ❌ WRONG — Unpredictable execution order
async customer(frm) {
frappe.call({ method: 'api' }).then(r => { frm.set_value('f', r.message); });
await doSomethingElse(); // Runs BEFORE .then()
}
// ✅ CORRECT — Consistent await pattern
async customer(frm) {
try {
let r = await frappe.call({ method: 'api' });
frm.set_value('f', r.message);
await doSomethingElse();
} catch (error) { console.error(error); }
}Why: Mixing patterns creates race conditions and unpredictable behavior.
---
11. Not Disabling Controls During Async Operations
// ❌ WRONG — User clicks multiple times
frm.add_custom_button(__('Process'), async () => {
await frappe.call({ method: 'myapp.api.process' });
frm.reload_doc();
});
// ✅ CORRECT — Disable during operation
frm.add_custom_button(__('Process'), async () => {
try {
frm.disable_save();
await frappe.call({ method: 'myapp.api.process', freeze: true, freeze_message: __('Processing...') });
frm.reload_doc();
} catch (error) {
frappe.msgprint(__('Processing failed'));
} finally {
frm.enable_save();
}
});Why: Duplicate clicks cause duplicate server operations.
---
12. Missing Translation Wrapper
// ❌ WRONG — Not translatable
frappe.throw('Customer is required');
// ✅ CORRECT
frappe.throw(__('Customer is required'));Why: Without __(), messages appear only in English regardless of user language.
---
Pre-Deploy Checklist
- [ ] All
frappe.throw()calls are ONLY invalidateevent - [ ] All async operations have try/catch
- [ ] All validation errors collected before throwing
- [ ] No
cur_frmusage anywhere - [ ] All user-facing strings use
__() - [ ] All server responses checked for null
- [ ] All frappe.call have error handler
- [ ] No
alert(),confirm(), orprompt() - [ ] No unconditioned
console.logstatements - [ ] Controls disabled during async operations
Client Script Error Examples — Real Scenarios
Complete diagnosis-oriented examples showing actual error messages, their root cause, and the fix.
---
Scenario 1: TypeError — Cannot Read Properties of Undefined
Error in console:
Uncaught TypeError: Cannot read properties of undefined (reading 'credit_limit')The broken code:
frappe.ui.form.on('Sales Order', {
async customer(frm) {
let r = await frappe.call({
method: 'myapp.api.get_customer',
args: { customer: frm.doc.customer }
});
// r.message is null when customer not found!
frm.set_value('credit_limit', r.message.credit_limit);
}
});Root cause: Server returned null for r.message — customer not found or API error.
The fix:
frappe.ui.form.on('Sales Order', {
async customer(frm) {
if (!frm.doc.customer) {
frm.set_value('credit_limit', 0);
return;
}
try {
let r = await frappe.call({
method: 'myapp.api.get_customer',
args: { customer: frm.doc.customer }
});
frm.set_value('credit_limit', r.message?.credit_limit || 0);
} catch (error) {
console.error('Customer fetch failed:', error);
frm.set_value('credit_limit', 0);
frappe.show_alert({
message: __('Could not load customer details'),
indicator: 'orange'
}, 5);
}
}
});---
Scenario 2: cur_frm Is Undefined in List View Context
Error in console:
Uncaught TypeError: Cannot read properties of undefined (reading 'set_value')
at cur_frm.set_value(...)The broken code:
// This JS was loaded globally (app.js) and runs in list context too
frappe.ui.form.on('Sales Order', {
customer(frm) {
cur_frm.set_value('territory', get_default_territory());
}
});Root cause: cur_frm is only set when a form is open. In list view, page view, or report context, it is undefined.
The fix:
frappe.ui.form.on('Sales Order', {
customer(frm) {
frm.set_value('territory', get_default_territory());
}
});Rule: ALWAYS use the frm parameter. NEVER use cur_frm.
---
Scenario 3: frappe.throw() Not Preventing Save
User reports: "I added validation but user can still save the form with invalid data."
The broken code:
frappe.ui.form.on('Sales Order', {
delivery_date(frm) {
if (frm.doc.delivery_date < frappe.datetime.get_today()) {
frappe.throw(__('Delivery date cannot be in the past'));
}
}
});Root cause: frappe.throw() in a field change event stops JavaScript execution but does NOT prevent the user from clicking Save. Only validate event blocks save.
The fix:
frappe.ui.form.on('Sales Order', {
delivery_date(frm) {
if (frm.doc.delivery_date < frappe.datetime.get_today()) {
frappe.msgprint({
message: __('Delivery date is in the past'),
indicator: 'orange'
});
}
},
validate(frm) {
if (frm.doc.delivery_date < frappe.datetime.get_today()) {
frappe.throw(__('Delivery date cannot be in the past'));
}
}
});---
Scenario 4: Unhandled Promise Rejection
Error in console:
Uncaught (in promise) Object { exc_type: "ValidationError", ... }The broken code:
frappe.ui.form.on('Sales Order', {
async customer(frm) {
// No try/catch — if server throws, this crashes silently
let r = await frappe.call({
method: 'myapp.api.validate_customer',
args: { customer: frm.doc.customer }
});
frm.set_value('validated', 1);
}
});Root cause: frappe.call with await throws when server returns error (e.g., frappe.throw() server-side). Without try/catch, the rejection is unhandled.
The fix:
frappe.ui.form.on('Sales Order', {
async customer(frm) {
if (!frm.doc.customer) return;
try {
let r = await frappe.call({
method: 'myapp.api.validate_customer',
args: { customer: frm.doc.customer }
});
frm.set_value('validated', 1);
} catch (error) {
console.error('Validation failed:', error);
frm.set_value('validated', 0);
// Parse server error message
if (error._server_messages) {
try {
let msgs = JSON.parse(error._server_messages);
let msg = JSON.parse(msgs[0]).message;
frappe.msgprint({ message: msg, indicator: 'red' });
} catch (e) {
frappe.msgprint(__('Validation failed'));
}
}
}
}
});---
Scenario 5: Child Table Row Access — Wrong Pattern
Error in console:
TypeError: Cannot read properties of undefined (reading 'item_code')The broken code:
frappe.ui.form.on('Sales Order Item', {
qty(frm, cdt, cdn) {
// Trying to find row by index — fragile and wrong
let idx = frm.doc.items.findIndex(r => r.name === cdn);
let row = frm.doc.items[idx];
row.amount = row.qty * row.rate; // Direct mutation — no UI refresh
}
});Root cause: Direct array access can fail when rows are reordered or deleted. Direct property mutation does not trigger UI refresh.
The fix:
frappe.ui.form.on('Sales Order Item', {
qty(frm, cdt, cdn) {
let row = frappe.get_doc(cdt, cdn);
let amount = (row.qty || 0) * (row.rate || 0);
frappe.model.set_value(cdt, cdn, 'amount', amount);
}
});---
Scenario 6: CSRF Token Error After Session Timeout
Error: 403 Forbidden with CSRF token mismatch.
The broken code:
// Manual XHR call without frappe.call
$.ajax({
url: '/api/method/myapp.api.process',
type: 'POST',
data: { name: frm.doc.name },
success: function(r) { frm.reload_doc(); }
});Root cause: Manual AJAX calls do not include the CSRF token header that Frappe requires. After session timeout, even some frappe.call requests may fail.
The fix:
// ALWAYS use frappe.call — handles CSRF automatically
frappe.call({
method: 'myapp.api.process',
args: { name: frm.doc.name },
callback(r) {
if (r.message) frm.reload_doc();
},
error(r) {
if (r.status === 401 || r.status === 403) {
frappe.msgprint(__('Session expired. Please refresh the page.'));
}
}
});---
Scenario 7: set_value in setup — Form Not Ready
Symptom: frm.set_value('company', 'Default Company') in setup silently does nothing.
The broken code:
frappe.ui.form.on('Sales Order', {
setup(frm) {
frm.set_value('company', 'Default Company'); // Ignored — form not loaded
}
});Root cause: setup fires before the form document is loaded from the server. set_value has no effect because the document fields don't exist yet.
The fix:
frappe.ui.form.on('Sales Order', {
setup(frm) {
// Only filters and formatters in setup
frm.set_query('customer', () => ({ filters: { disabled: 0 } }));
},
onload(frm) {
// Set defaults for new documents
if (frm.is_new()) {
frm.set_value('company', 'Default Company');
}
}
});---
Scenario 8: Mixing .then() and await
Symptom: Code runs in wrong order — doSomethingElse() executes before frappe.call completes.
The broken code:
frappe.ui.form.on('Sales Order', {
async customer(frm) {
frappe.call({
method: 'myapp.api.get_data',
args: { customer: frm.doc.customer }
}).then(r => {
frm.set_value('credit_limit', r.message.limit);
});
// This runs BEFORE .then() callback!
await frm.save();
}
});The fix:
frappe.ui.form.on('Sales Order', {
async customer(frm) {
try {
let r = await frappe.call({
method: 'myapp.api.get_data',
args: { customer: frm.doc.customer }
});
if (r.message) {
await frm.set_value('credit_limit', r.message.limit);
}
await frm.save();
} catch (error) {
console.error('Error:', error);
}
}
});Rule: NEVER mix .then() and await in the same function. Pick one pattern.
---
HTTP Status Code Quick Reference
| Status | Meaning in Frappe | Common Cause |
|---|---|---|
| 401 | Session expired | User not logged in or token expired |
| 403 | Permission denied | Method not whitelisted or role missing |
| 404 | Not found | Method path wrong or document deleted |
| 417 | Expectation Failed | Server-side frappe.throw() |
| 429 | Rate limited | Too many requests |
| 500 | Server error | Unhandled Python exception |
| 502/503 | Gateway error | Server overloaded or restarting |
Client Script Error Handling Patterns
Reusable patterns for defensive error handling in Frappe Client Scripts.
---
Pattern 1: Safe Server Call Wrapper
/**
* Wrapper for frappe.call with consistent error handling.
* ALWAYS use this instead of raw frappe.call for user-triggered actions.
*/
async function safeCall(options) {
try {
const r = await frappe.call(options);
return r.message;
} catch (error) {
console.error(`API Error [${options.method}]:`, error);
if (!navigator.onLine) {
frappe.msgprint({
title: __('No Connection'),
message: __('Check your internet connection and try again.'),
indicator: 'red'
});
} else if (error.status === 401 || error.status === 403) {
frappe.msgprint({
title: __('Access Denied'),
message: __('Session may have expired. Please refresh the page.'),
indicator: 'red'
});
} else if (error.status >= 500) {
frappe.msgprint({
title: __('Server Error'),
message: __('Server error occurred. Please try again later.'),
indicator: 'red'
});
} else if (error._server_messages) {
// Parse server-side frappe.throw() message
try {
let msgs = JSON.parse(error._server_messages);
let msg = JSON.parse(msgs[0]).message;
frappe.msgprint({ message: msg, indicator: 'red' });
} catch (e) {
frappe.msgprint(__('An error occurred'));
}
}
return null; // Caller checks for null
}
}
// Usage
frappe.ui.form.on('Sales Order', {
async customer(frm) {
if (!frm.doc.customer) return;
let data = await safeCall({
method: 'myapp.api.get_customer_details',
args: { customer: frm.doc.customer }
});
if (data) {
frm.set_value('credit_limit', data.credit_limit || 0);
}
}
});---
Pattern 2: Validation Error Collector
frappe.ui.form.on('Sales Order', {
validate(frm) {
let errors = [];
// Required fields
if (!frm.doc.customer) errors.push(__('Customer is required'));
if (!frm.doc.delivery_date) errors.push(__('Delivery Date is required'));
// Date validation
if (frm.doc.delivery_date && frm.doc.delivery_date < frappe.datetime.get_today()) {
errors.push(__('Delivery Date cannot be in the past'));
}
// Child table validation
if (!frm.doc.items || frm.doc.items.length === 0) {
errors.push(__('At least one item is required'));
} else {
frm.doc.items.forEach((row, idx) => {
if (!row.item_code) errors.push(__('Row {0}: Item is required', [idx + 1]));
if ((row.qty || 0) <= 0) errors.push(__('Row {0}: Qty must be positive', [idx + 1]));
});
}
if (errors.length) {
frappe.throw({
title: __('Please fix the following'),
message: errors.join('<br>')
});
}
}
});---
Pattern 3: Async Button with Error Boundary
frappe.ui.form.on('Sales Order', {
refresh(frm) {
if (frm.doc.docstatus === 1) {
frm.add_custom_button(__('Process'), async () => {
await withErrorBoundary(frm, async () => {
await frappe.call({
method: 'myapp.api.process_order',
args: { name: frm.doc.name },
freeze: true,
freeze_message: __('Processing...')
});
frappe.show_alert({ message: __('Done'), indicator: 'green' }, 3);
frm.reload_doc();
});
});
}
}
});
async function withErrorBoundary(frm, asyncFn) {
try {
frm.disable_save();
await asyncFn();
} catch (error) {
console.error('Action failed:', error);
frappe.msgprint({
title: __('Error'),
message: __('Operation failed. Please try again.'),
indicator: 'red'
});
} finally {
frm.enable_save();
}
}---
Pattern 4: Graceful Degradation — Optional Data
frappe.ui.form.on('Sales Order', {
async refresh(frm) {
// Try to load dashboard data, but don't fail if unavailable
try {
let stock = await frappe.call({
method: 'myapp.api.get_stock_summary',
args: { items: (frm.doc.items || []).map(r => r.item_code).filter(Boolean) }
});
if (stock.message) {
renderStockDashboard(frm, stock.message);
}
} catch (error) {
console.warn('Stock dashboard unavailable:', error);
frm.dashboard.set_headline(__('Stock info unavailable'), 'orange');
}
}
});---
Pattern 5: Recursion Guard with Flags
frappe.ui.form.on('Sales Order', {
discount_percent(frm) {
if (frm.flags.recalculating) return;
frm.flags.recalculating = true;
try {
let total = calculateTotal(frm);
frm.set_value('grand_total', total);
} finally {
frm.flags.recalculating = false;
}
},
grand_total(frm) {
if (frm.flags.recalculating) return;
// Respond to manual grand_total changes
}
});---
Pattern 6: Retry with Exponential Backoff
async function fetchWithRetry(method, args, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
let r = await frappe.call({ method, args });
return r.message;
} catch (error) {
if (attempt === maxRetries) throw error;
console.warn(`Attempt ${attempt} failed, retrying...`);
await new Promise(resolve =>
setTimeout(resolve, Math.pow(2, attempt) * 1000)
);
}
}
}---
Pattern 7: Confirmation Before Destructive Action
async function confirmAndExecute(frm, message, action) {
return new Promise((resolve) => {
frappe.confirm(
message,
async () => {
try {
await action(frm);
resolve(true);
} catch (error) {
console.error('Action failed:', error);
frappe.msgprint({ message: __('Action failed'), indicator: 'red' });
resolve(false);
}
},
() => resolve(false)
);
});
}
// Usage
frm.add_custom_button(__('Cancel Order'), async () => {
await confirmAndExecute(
frm,
__('Are you sure you want to cancel this order?'),
async (frm) => {
await frappe.call({ method: 'frappe.client.cancel', args: { doctype: frm.doctype, name: frm.doc.name } });
frm.reload_doc();
}
);
});---
Pattern 8: Conditional Debug Logger
const DEBUG = frappe.boot.developer_mode;
const log = {
info: (...args) => { if (DEBUG) console.log('[MyApp]', ...args); },
warn: (...args) => { if (DEBUG) console.warn('[MyApp]', ...args); },
error: (...args) => { console.error('[MyApp]', ...args); } // Always log errors
};
// Usage — no console.log in production
frappe.ui.form.on('Sales Order', {
validate(frm) {
log.info('Validating:', frm.doc.name);
}
});---
Pattern 9: Batch Processing with Progress
async function processBatch(frm, items, processFn) {
let results = { success: 0, failed: 0, errors: [] };
frappe.show_progress(__('Processing'), 0, items.length);
for (let i = 0; i < items.length; i++) {
try {
await processFn(items[i]);
results.success++;
} catch (error) {
results.failed++;
results.errors.push({ row: i + 1, error: error.message || 'Unknown' });
}
frappe.show_progress(__('Processing'), i + 1, items.length);
}
frappe.hide_progress();
if (results.failed === 0) {
frappe.msgprint({ message: __('All {0} items processed', [results.success]), indicator: 'green' });
} else {
let errorList = results.errors.map(e => __('Row {0}: {1}', [e.row, e.error])).join('<br>');
frappe.msgprint({
title: __('Completed with Errors'),
message: __('Success: {0}, Failed: {1}', [results.success, results.failed]) + '<br><br>' + errorList,
indicator: 'orange'
});
}
return results;
}---
Pattern 10: Loading State for Async Field Change
frappe.ui.form.on('Sales Order', {
async item_code(frm) {
if (!frm.doc.item_code) return;
frm.set_df_property('rate', 'read_only', 1);
frm.set_df_property('rate', 'description', __('Loading price...'));
try {
let r = await frappe.call({
method: 'myapp.api.get_price',
args: { item_code: frm.doc.item_code, customer: frm.doc.customer }
});
if (r.message) {
await frm.set_value('rate', r.message.rate);
frm.set_df_property('rate', 'description', '');
} else {
frm.set_df_property('rate', 'description', __('No price found — enter manually'));
}
} catch (error) {
console.error('Price lookup failed:', error);
frm.set_df_property('rate', 'description', __('Price lookup failed'));
} finally {
frm.set_df_property('rate', 'read_only', 0);
}
}
});