erpnext-errors-clientscripts
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseERPNext Client Scripts - Error Handling
ERPNext 客户端脚本 - 错误处理
This skill covers error handling patterns for Client Scripts. For syntax, see . For implementation workflows, see .
erpnext-syntax-clientscriptserpnext-impl-clientscriptsVersion: v14/v15/v16 compatible
本技能涵盖客户端脚本的错误处理模式。语法相关内容请参考。实现工作流相关内容请参考。
erpnext-syntax-clientscriptserpnext-impl-clientscripts版本:兼容v14/v15/v16版本
Main Decision: How to Handle the Error?
核心决策:如何处理错误?
┌─────────────────────────────────────────────────────────────────────────┐
│ WHAT TYPE OF ERROR ARE YOU HANDLING? │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ► Validation error (must prevent save)? │
│ └─► frappe.throw() in validate event │
│ │
│ ► Warning (inform user, allow continue)? │
│ └─► frappe.msgprint() with indicator │
│ │
│ ► Server call might fail? │
│ └─► try/catch with async/await OR callback error handling │
│ │
│ ► Field value invalid (not blocking)? │
│ └─► frm.set_intro() or field description │
│ │
│ ► Need to debug/trace? │
│ └─► console.log/warn/error + frappe.show_alert for dev │
│ │
│ ► Unexpected error in any code? │
│ └─► Wrap in try/catch, log error, show user-friendly message │
│ │
└─────────────────────────────────────────────────────────────────────────┘┌─────────────────────────────────────────────────────────────────────────┐
│ 你要处理的是哪种类型的错误? │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ► 验证错误(必须阻止保存)? │
│ └─► 在validate事件中使用frappe.throw() │
│ │
│ ► 警告(告知用户,允许继续操作)? │
│ └─► 带指示器的frappe.msgprint() │
│ │
│ ► 服务器调用可能失败? │
│ └─► 结合async/await或回调函数的try/catch错误处理 │
│ │
│ ► 字段值无效(不阻止操作)? │
│ └─► frm.set_intro() 或字段描述 │
│ │
│ ► 需要调试/追踪? │
│ └─► console.log/warn/error + 面向开发者的frappe.show_alert │
│ │
│ ► 代码中出现意外错误? │
│ └─► 用try/catch包裹,记录错误,显示用户友好型提示 │
│ │
└─────────────────────────────────────────────────────────────────────────┘Error Feedback Methods
错误反馈方法
Quick Reference
快速参考
| Method | Blocks Save? | User Action | Use For |
|---|---|---|---|
| ✅ YES | Must dismiss | Validation errors |
| ❌ NO | Must dismiss | Important info/warnings |
| ❌ NO | Auto-dismiss | Success/info feedback |
| ❌ NO | None | Form-level warnings |
| ❌ NO | None | Status indicators |
| ❌ NO | None | Debugging only |
| 方法 | 阻止保存? | 用户操作 | 适用场景 |
|---|---|---|---|
| ✅ 是 | 必须关闭 | 验证错误 |
| ❌ 否 | 必须关闭 | 重要信息/警告 |
| ❌ 否 | 自动关闭 | 成功/信息反馈 |
| ❌ 否 | 无 | 表单级警告 |
| ❌ 否 | 无 | 状态指示器 |
| ❌ 否 | 无 | 仅用于调试 |
frappe.throw() - Blocking Error
frappe.throw() - 阻塞式错误
javascript
// Basic throw - stops execution, prevents save
frappe.throw(__('Customer is required'));
// With title
frappe.throw({
title: __('Validation Error'),
message: __('Amount cannot be negative')
});
// With indicator color
frappe.throw({
message: __('Credit limit exceeded'),
indicator: 'red'
});CRITICAL: Only use in event to prevent save. Using it elsewhere stops script execution but doesn't prevent form actions.
frappe.throw()validatejavascript
// 基础抛出 - 终止执行,阻止保存
frappe.throw(__('客户为必填项'));
// 带标题
frappe.throw({
title: __('验证错误'),
message: __('金额不能为负数')
});
// 带指示器颜色
frappe.throw({
message: __('信用额度已超出'),
indicator: 'red'
});关键提示:仅在事件中使用来阻止保存。在其他位置使用会终止脚本执行,但无法阻止表单操作。
validatefrappe.throw()frappe.msgprint() - Non-Blocking Alert
frappe.msgprint() - 非阻塞式提示
javascript
// Simple message
frappe.msgprint(__('Document will be processed'));
// With title and indicator
frappe.msgprint({
title: __('Warning'),
message: __('Stock is running low'),
indicator: 'orange'
});
// With primary action button
frappe.msgprint({
title: __('Confirm Action'),
message: __('This will archive 50 records. Continue?'),
primary_action: {
label: __('Yes, Archive'),
action: () => {
// perform action
frappe.hide_msgprint();
}
}
});javascript
// 简单提示
frappe.msgprint(__('文档将被处理'));
// 带标题和指示器
frappe.msgprint({
title: __('警告'),
message: __('库存即将不足'),
indicator: 'orange'
});
// 带主要操作按钮
frappe.msgprint({
title: __('确认操作'),
message: __('这将归档50条记录。是否继续?'),
primary_action: {
label: __('是,归档'),
action: () => {
// 执行操作
frappe.hide_msgprint();
}
}
});frappe.show_alert() - Toast Notification
frappe.show_alert() - 提示通知
javascript
// Success (green, auto-dismiss)
frappe.show_alert({
message: __('Saved successfully'),
indicator: 'green'
}, 3); // 3 seconds
// Warning (orange)
frappe.show_alert({
message: __('Some items are out of stock'),
indicator: 'orange'
}, 5);
// Error (red)
frappe.show_alert({
message: __('Failed to fetch data'),
indicator: 'red'
}, 5);javascript
// 成功(绿色,自动关闭)
frappe.show_alert({
message: __('保存成功'),
indicator: 'green'
}, 3); // 3秒后关闭
// 警告(橙色)
frappe.show_alert({
message: __('部分商品缺货'),
indicator: 'orange'
}, 5);
// 错误(红色)
frappe.show_alert({
message: __('获取数据失败'),
indicator: 'red'
}, 5);Error Handling Patterns
错误处理模式
Pattern 1: Synchronous Validation
模式1:同步验证
javascript
frappe.ui.form.on('Sales Order', {
validate(frm) {
// Multiple validations - collect errors
let errors = [];
if (!frm.doc.customer) {
errors.push(__('Customer is required'));
}
if (frm.doc.grand_total <= 0) {
errors.push(__('Total must be greater than zero'));
}
if (!frm.doc.items || frm.doc.items.length === 0) {
errors.push(__('At least one item is required'));
}
// Throw all errors at once
if (errors.length > 0) {
frappe.throw({
title: __('Validation Errors'),
message: errors.join('<br>')
});
}
}
});javascript
frappe.ui.form.on('Sales Order', {
validate(frm) {
// 多验证项 - 收集错误
let errors = [];
if (!frm.doc.customer) {
errors.push(__('客户为必填项'));
}
if (frm.doc.grand_total <= 0) {
errors.push(__('总金额必须大于0'));
}
if (!frm.doc.items || frm.doc.items.length === 0) {
errors.push(__('至少需要一个商品'));
}
// 一次性抛出所有错误
if (errors.length > 0) {
frappe.throw({
title: __('验证错误'),
message: errors.join('<br>')
});
}
}
});Pattern 2: Async Server Call with Error Handling
模式2:带错误处理的异步服务器调用
javascript
frappe.ui.form.on('Sales Order', {
async customer(frm) {
if (!frm.doc.customer) return;
try {
let r = await frappe.call({
method: 'myapp.api.get_customer_details',
args: { customer: frm.doc.customer }
});
if (r.message) {
frm.set_value('credit_limit', r.message.credit_limit);
}
} catch (error) {
console.error('Failed to fetch customer:', error);
frappe.show_alert({
message: __('Could not load customer details'),
indicator: 'red'
}, 5);
// Don't throw - allow user to continue
}
}
});javascript
frappe.ui.form.on('Sales Order', {
async customer(frm) {
if (!frm.doc.customer) return;
try {
let r = await frappe.call({
method: 'myapp.api.get_customer_details',
args: { customer: frm.doc.customer }
});
if (r.message) {
frm.set_value('credit_limit', r.message.credit_limit);
}
} catch (error) {
console.error('获取客户信息失败:', error);
frappe.show_alert({
message: __('无法加载客户详情'),
indicator: 'red'
}, 5);
// 不抛出错误 - 允许用户继续操作
}
}
});Pattern 3: Async Validation with Server Check
模式3:带服务器检查的异步验证
javascript
frappe.ui.form.on('Sales Order', {
async validate(frm) {
// Server-side validation
try {
let r = await frappe.call({
method: 'myapp.api.validate_order',
args: {
customer: frm.doc.customer,
total: frm.doc.grand_total
}
});
if (r.message && !r.message.valid) {
frappe.throw({
title: __('Validation Failed'),
message: r.message.error
});
}
} catch (error) {
// Server error - decide: block or allow?
console.error('Validation call failed:', error);
// Option 1: Block save on server error (safer)
frappe.throw(__('Could not validate. Please try again.'));
// Option 2: Allow save with warning (use with caution)
// frappe.show_alert({
// message: __('Validation skipped due to server error'),
// indicator: 'orange'
// }, 5);
}
}
});javascript
frappe.ui.form.on('Sales Order', {
async validate(frm) {
// 服务器端验证
try {
let r = await frappe.call({
method: 'myapp.api.validate_order',
args: {
customer: frm.doc.customer,
total: frm.doc.grand_total
}
});
if (r.message && !r.message.valid) {
frappe.throw({
title: __('验证失败'),
message: r.message.error
});
}
} catch (error) {
// 服务器错误 - 决定:阻止还是允许?
console.error('验证调用失败:', error);
// 选项1:服务器错误时阻止保存(更安全)
frappe.throw(__('无法完成验证,请重试。'));
// 选项2:允许保存并显示警告(谨慎使用)
// frappe.show_alert({
// message: __('因服务器错误跳过验证'),
// indicator: 'orange'
// }, 5);
}
}
});Pattern 4: frappe.call Callback Error Handling
模式4:frappe.call 回调函数错误处理
javascript
// When not using async/await
frappe.call({
method: 'myapp.api.process_data',
args: { doc_name: frm.doc.name },
freeze: true,
freeze_message: __('Processing...'),
callback: (r) => {
if (r.message) {
frappe.show_alert({
message: __('Processing complete'),
indicator: 'green'
});
frm.reload_doc();
}
},
error: (r) => {
// Server returned error (4xx, 5xx)
console.error('API Error:', r);
frappe.msgprint({
title: __('Error'),
message: __('Processing failed. Please try again.'),
indicator: 'red'
});
}
});javascript
// 不使用async/await时
frappe.call({
method: 'myapp.api.process_data',
args: { doc_name: frm.doc.name },
freeze: true,
freeze_message: __('处理中...'),
callback: (r) => {
if (r.message) {
frappe.show_alert({
message: __('处理完成'),
indicator: 'green'
});
frm.reload_doc();
}
},
error: (r) => {
// 服务器返回错误(4xx, 5xx)
console.error('API错误:', r);
frappe.msgprint({
title: __('错误'),
message: __('处理失败,请重试。'),
indicator: 'red'
});
}
});Pattern 5: Child Table Validation
模式5:子表验证
javascript
frappe.ui.form.on('Sales Invoice', {
validate(frm) {
let errors = [];
(frm.doc.items || []).forEach((row, idx) => {
if (!row.item_code) {
errors.push(__('Row {0}: Item is required', [idx + 1]));
}
if (row.qty <= 0) {
errors.push(__('Row {0}: Quantity must be positive', [idx + 1]));
}
if (row.rate < 0) {
errors.push(__('Row {0}: Rate cannot be negative', [idx + 1]));
}
});
if (errors.length > 0) {
frappe.throw({
title: __('Item Errors'),
message: errors.join('<br>')
});
}
}
});javascript
frappe.ui.form.on('Sales Invoice', {
validate(frm) {
let errors = [];
(frm.doc.items || []).forEach((row, idx) => {
if (!row.item_code) {
errors.push(__('第{0}行:商品为必填项', [idx + 1]));
}
if (row.qty <= 0) {
errors.push(__('第{0}行:数量必须为正数', [idx + 1]));
}
if (row.rate < 0) {
errors.push(__('第{0}行:单价不能为负数', [idx + 1]));
}
});
if (errors.length > 0) {
frappe.throw({
title: __('商品错误'),
message: errors.join('<br>')
});
}
}
});Pattern 6: Graceful Degradation
模式6:优雅降级
javascript
frappe.ui.form.on('Sales Order', {
async refresh(frm) {
// Try to load extra 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) }
});
if (stock.message) {
render_stock_dashboard(frm, stock.message);
}
} catch (error) {
// Log but don't disturb user
console.warn('Stock dashboard unavailable:', error);
// Optionally show subtle indicator
frm.dashboard.set_headline(
__('Stock info unavailable'),
'orange'
);
}
}
});See:for more error handling patterns.references/patterns.md
javascript
frappe.ui.form.on('Sales Order', {
async refresh(frm) {
// 尝试加载额外数据,但不可用时不中断流程
try {
let stock = await frappe.call({
method: 'myapp.api.get_stock_summary',
args: { items: frm.doc.items.map(r => r.item_code) }
});
if (stock.message) {
render_stock_dashboard(frm, stock.message);
}
} catch (error) {
// 记录日志但不打扰用户
console.warn('库存仪表盘不可用:', error);
// 可选:显示细微指示器
frm.dashboard.set_headline(
__('库存信息不可用'),
'orange'
);
}
}
});参考:获取更多错误处理模式。references/patterns.md
Debugging Techniques
调试技巧
Console Logging
控制台日志
javascript
// Development debugging
frappe.ui.form.on('Sales Order', {
customer(frm) {
console.log('Customer changed:', frm.doc.customer);
console.log('Full doc:', JSON.parse(JSON.stringify(frm.doc)));
// Trace child table
console.table(frm.doc.items);
}
});javascript
// 开发调试
frappe.ui.form.on('Sales Order', {
customer(frm) {
console.log('客户已变更:', frm.doc.customer);
console.log('完整文档:', JSON.parse(JSON.stringify(frm.doc)));
// 追踪子表
console.table(frm.doc.items);
}
});Conditional Debugging
条件调试
javascript
// Only log in development
const DEBUG = frappe.boot.developer_mode;
function debugLog(...args) {
if (DEBUG) {
console.log('[MyApp]', ...args);
}
}
frappe.ui.form.on('Sales Order', {
validate(frm) {
debugLog('Validating:', frm.doc.name);
// validation logic
}
});javascript
// 仅在开发环境中记录日志
const DEBUG = frappe.boot.developer_mode;
function debugLog(...args) {
if (DEBUG) {
console.log('[MyApp]', ...args);
}
}
frappe.ui.form.on('Sales Order', {
validate(frm) {
debugLog('正在验证:', frm.doc.name);
// 验证逻辑
}
});Error Stack Traces
错误堆栈追踪
javascript
try {
riskyOperation();
} catch (error) {
console.error('Error details:', {
message: error.message,
stack: error.stack,
doc: frm.doc.name
});
// User-friendly message (no technical details)
frappe.msgprint({
title: __('Error'),
message: __('An unexpected error occurred. Please contact support.'),
indicator: 'red'
});
}javascript
try {
riskyOperation();
} catch (error) {
console.error('错误详情:', {
message: error.message,
stack: error.stack,
doc: frm.doc.name
});
// 用户友好型提示(无技术细节)
frappe.msgprint({
title: __('错误'),
message: __('发生了意外错误,请联系支持人员。'),
indicator: 'red'
});
}Critical Rules
关键规则
✅ ALWAYS
✅ 必须遵守
- Wrap async calls in try/catch - Uncaught Promise rejections crash silently
- Use for error messages - All user-facing text must be translatable
__() - Log errors to console - Helps debugging without exposing to users
- Collect multiple validation errors - Don't throw on first error
- Provide actionable error messages - Tell user how to fix it
- 异步调用必须包裹在try/catch中 - 未捕获的Promise拒绝会静默崩溃
- 错误消息使用__()包裹 - 所有面向用户的文本必须支持翻译
- 将错误记录到控制台 - 便于调试且不暴露给用户
- 收集多个验证错误 - 不要在第一个错误时就抛出
- 提供可操作的错误提示 - 告知用户如何解决问题
❌ NEVER
❌ 禁止操作
- Don't expose technical errors to users - Catch and translate
- Don't use outside validate - It stops execution but doesn't prevent save
frappe.throw() - Don't ignore server call failures - Always handle error callback
- Don't use or
alert()- Use frappe methods insteadconfirm() - Don't leave in production - Use conditional debugging
console.log
- 不要向用户暴露技术错误 - 捕获并转换为友好提示
- 不要在validate事件外使用frappe.throw() - 它会终止执行但无法阻止保存
- 不要忽略服务器调用失败 - 始终处理错误回调
- 不要使用alert()或confirm() - 改用Frappe提供的方法
- 不要在生产环境中保留console.log - 使用条件调试
Quick Reference: Error Message Quality
快速参考:错误提示质量
javascript
// ❌ BAD - Technical, not actionable
frappe.throw('NullPointerException in line 42');
frappe.throw('Query failed');
frappe.throw('Error');
// ✅ GOOD - Clear, actionable
frappe.throw(__('Please select a customer before adding items'));
frappe.throw(__('Amount {0} exceeds credit limit of {1}', [amount, limit]));
frappe.throw(__('Could not save. Please check your internet connection and try again.'));javascript
// ❌ 错误示例 - 技术化,无操作指引
frappe.throw('NullPointerException in line 42');
frappe.throw('Query failed');
frappe.throw('Error');
// ✅ 正确示例 - 清晰、可操作
frappe.throw(__('请先选择客户再添加商品'));
frappe.throw(__('金额{0}超出了信用额度{1}', [amount, limit]));
frappe.throw(__('保存失败,请检查网络连接后重试。'));Reference Files
参考文件
| File | Contents |
|---|---|
| Complete error handling patterns |
| Full working examples |
| Common mistakes to avoid |
| 文件 | 内容 |
|---|---|
| 完整的错误处理模式 |
| 完整的可用示例 |
| 需避免的常见错误 |
See Also
另请参阅
- - Client Script syntax
erpnext-syntax-clientscripts - - Implementation workflows
erpnext-impl-clientscripts - - Server-side error handling
erpnext-errors-serverscripts
- - 客户端脚本语法
erpnext-syntax-clientscripts - - 实现工作流
erpnext-impl-clientscripts - - 服务器端错误处理
erpnext-errors-serverscripts