erpnext-errors-clientscripts

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

ERPNext Client Scripts - Error Handling

ERPNext 客户端脚本 - 错误处理

This skill covers error handling patterns for Client Scripts. For syntax, see
erpnext-syntax-clientscripts
. For implementation workflows, see
erpnext-impl-clientscripts
.
Version: v14/v15/v16 compatible

本技能涵盖客户端脚本的错误处理模式。语法相关内容请参考
erpnext-syntax-clientscripts
。实现工作流相关内容请参考
erpnext-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

快速参考

MethodBlocks Save?User ActionUse For
frappe.throw()
✅ YESMust dismissValidation errors
frappe.msgprint()
❌ NOMust dismissImportant info/warnings
frappe.show_alert()
❌ NOAuto-dismissSuccess/info feedback
frm.set_intro()
❌ NONoneForm-level warnings
frm.dashboard.set_headline()
❌ NONoneStatus indicators
console.error()
❌ NONoneDebugging only
方法阻止保存?用户操作适用场景
frappe.throw()
✅ 是必须关闭验证错误
frappe.msgprint()
❌ 否必须关闭重要信息/警告
frappe.show_alert()
❌ 否自动关闭成功/信息反馈
frm.set_intro()
❌ 否表单级警告
frm.dashboard.set_headline()
❌ 否状态指示器
console.error()
❌ 否仅用于调试

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
frappe.throw()
in
validate
event to prevent save. Using it elsewhere stops script execution but doesn't prevent form actions.
javascript
// 基础抛出 - 终止执行,阻止保存
frappe.throw(__('客户为必填项'));

// 带标题
frappe.throw({
    title: __('验证错误'),
    message: __('金额不能为负数')
});

// 带指示器颜色
frappe.throw({
    message: __('信用额度已超出'),
    indicator: 'red'
});
关键提示:仅在
validate
事件中使用
frappe.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:
references/patterns.md
for more error handling patterns.

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

✅ 必须遵守

  1. Wrap async calls in try/catch - Uncaught Promise rejections crash silently
  2. Use
    __()
    for error messages
    - All user-facing text must be translatable
  3. Log errors to console - Helps debugging without exposing to users
  4. Collect multiple validation errors - Don't throw on first error
  5. Provide actionable error messages - Tell user how to fix it
  1. 异步调用必须包裹在try/catch中 - 未捕获的Promise拒绝会静默崩溃
  2. 错误消息使用__()包裹 - 所有面向用户的文本必须支持翻译
  3. 将错误记录到控制台 - 便于调试且不暴露给用户
  4. 收集多个验证错误 - 不要在第一个错误时就抛出
  5. 提供可操作的错误提示 - 告知用户如何解决问题

❌ NEVER

❌ 禁止操作

  1. Don't expose technical errors to users - Catch and translate
  2. Don't use
    frappe.throw()
    outside validate
    - It stops execution but doesn't prevent save
  3. Don't ignore server call failures - Always handle error callback
  4. Don't use
    alert()
    or
    confirm()
    - Use frappe methods instead
  5. Don't leave
    console.log
    in production
    - Use conditional debugging

  1. 不要向用户暴露技术错误 - 捕获并转换为友好提示
  2. 不要在validate事件外使用frappe.throw() - 它会终止执行但无法阻止保存
  3. 不要忽略服务器调用失败 - 始终处理错误回调
  4. 不要使用alert()或confirm() - 改用Frappe提供的方法
  5. 不要在生产环境中保留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

参考文件

FileContents
references/patterns.md
Complete error handling patterns
references/examples.md
Full working examples
references/anti-patterns.md
Common mistakes to avoid

文件内容
references/patterns.md
完整的错误处理模式
references/examples.md
完整的可用示例
references/anti-patterns.md
需避免的常见错误

See Also

另请参阅

  • erpnext-syntax-clientscripts
    - Client Script syntax
  • erpnext-impl-clientscripts
    - Implementation workflows
  • erpnext-errors-serverscripts
    - Server-side error handling
  • erpnext-syntax-clientscripts
    - 客户端脚本语法
  • erpnext-impl-clientscripts
    - 实现工作流
  • erpnext-errors-serverscripts
    - 服务器端错误处理