关于 frappe-errors-clientscripts
面向 Frappe 开发者的客户端脚本错误诊断指南,将常见错误与原因、修复方法对应起来。内容覆盖异步调用、子表访问、事件时机及调试工具。
主要功能
- 01按错误信息定位原因
- 02提供常见问题修复模式
- 03说明异步调用错误处理
- 04指导子表事件中的数据访问
- 05列出浏览器调试工具
适用场景
- 01排查表单字段未定义错误
- 02修复 frappe.call 静默失败
- 03诊断子表行访问问题
- 04处理事件循环或保存校验问题
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ impertio-studio/ frappe_claude_skill_package --skill frappe-errors-clientscripts安装后在 Claude Code 中输入/frappe-errors-clientscripts确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/anti-patterns.mdexamples.mdpatterns.md详细介绍
客户端脚本错误 — 诊断与解决
交叉引用:frappe-syntax-clientscripts(语法)、frappe-impl-clientscripts(工作流)、frappe-errors-serverscripts(服务器端)。
错误诊断流程图
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
错误消息 → 原因 → 修复方法表
| 错误消息 | 原因 | 修复方法 |
|---|---|---|
TypeError: Cannot read properties of undefined (reading 'fieldname') | DocType 中不存在该字段,或文档尚未加载 | 始终在访问字段前检查 frm.doc 是否存在 |
TypeError: frm.set_value is not a function | 使用了未定义的 cur_frm 快捷方式 | 始终使用事件处理程序中的 frm 参数 |
Uncaught (in promise) | frappe.call 的异步拒绝未处理 | 始终使用 try/catch 包装异步调用 |
CSRFTokenError / 403 with CSRF | 会话超时后令牌不匹配 | 始终使用 frappe.call()(会自动处理 CSRF) |
Not permitted / frappe.call 返回 403 | 服务器方法缺少 @frappe.whitelist() | 始终为 API 方法添加 @frappe.whitelist() 装饰器 |
frappe.throw() not preventing save | 在 validate 事件之外使用了 frappe.throw() | 仅在 validate 中使用 frappe.throw() |
set_query 中出现 field not found: xyz | fieldname 拼写错误,或字段不在子表中 | 对照 DocType 定义确认准确的 fieldname |
row.item_code is undefined | 子表行访问方式错误 — locals 尚未同步 | 在子表事件中使用 frappe.get_doc(cdt, cdn) |
frm.set_value not working | 在表单完全加载前的 setup 中调用 | 将字段设置逻辑移到 refresh 事件 |
Maximum call stack exceeded | 触发循环 — 字段更改触发了自身处理程序 | 使用 frm.flags 守卫打破递归 |
关键错误模式
1. cur_frm 与 frm:新手最常犯的错误
// ❌ 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');
}
});
规则:绝不要使用 cur_frm。始终使用传递给每个事件处理程序的 frm 参数。
2. Async/Await:没有 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. 子表访问:错误模式
// ❌ 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. 时机:setup 与 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() 的作用范围:仅在 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. 使用标志防止递归
// ❌ 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;
}
});
调试工具
| 工具 | 使用方法 | 适用场景 |
|---|---|---|
| 浏览器控制台(F12) | console.log(frm.doc) | 检查表单状态 |
console.table() | console.table(frm.doc.items) | 查看子表行 |
JSON.parse(JSON.stringify(frm.doc)) | 深度克隆以创建快照 | 避免控制台中的循环引用 |
frappe.boot.developer_mode | 检查是否开启开发者模式 | 有条件地记录调试日志 |
frappe.ui.toolbar.clear_cache() | 清除客户端缓存 | 部署脚本更改后 |
| Network 标签页(F12) | 筛选 XHR 请求 | 检查 frappe.call 负载 |
frappe.show_alert({message: 'debug', indicator: 'blue'}, 5) | 在 UI 中显示可视化调试信息 | 无需控制台即可快速反馈 |
始终遵循 / 绝不要违反的规则
始终遵循
- 使用
frm参数 — 绝不要使用cur_frm[v14+] - 用 try/catch 包装异步 frappe.call — 未处理的拒绝会静默失败
- 所有面向用户的字符串都使用
__()— 翻译所必需 - 先收集多个验证错误,再调用
frappe.throw() - 使用
frappe.get_doc(cdt, cdn)在事件中访问子表行 - 仅在
validate中使用frappe.throw()以阻止保存 - 访问服务器响应属性前,检查
r.message是否为 null - 在子表事件中使用
frappe.model.set_value(cdt, cdn, field, value)
绝对不要
- 绝对不要使用
alert()、confirm()或prompt()— 使用 frappe.msgprint / frappe.confirm - 绝对不要向用户暴露堆栈跟踪 — 记录到控制台,并显示友好的消息
- 绝对不要使用
cur_frm— 它不可靠,在许多上下文中未定义 - 绝对不要在生产环境中留下
console.log— 使用条件检查frappe.boot.developer_mode - 绝对不要在同一个函数中混用
.then()和await— 选择一种模式 - 绝对不要在
setup中调用frm.set_value— 表单尚未就绪;请使用refresh或onload - 使用回调模式时,绝对不要忽略
frappe.call的error回调
参考文件
| 文件 | 内容 |
|---|---|
references/examples.md | 真实错误场景及其诊断 |
references/anti-patterns.md | 常见错误及修复前后对比 |
references/patterns.md | 防御性错误处理模式 |
评论
支持 Markdown