erpnext-errors-controllers

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

ERPNext Controllers - Error Handling

ERPNext Controllers - 错误处理

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

本技能介绍Document Controllers的错误处理模式。语法相关内容请参考
erpnext-syntax-controllers
,实现工作流请参考
erpnext-impl-controllers
版本:兼容v14/v15/v16

Controllers vs Server Scripts: Error Handling

控制器与服务器脚本:错误处理

┌─────────────────────────────────────────────────────────────────────┐
│ CONTROLLERS HAVE FULL PYTHON POWER                                  │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│ ✅ try/except blocks - Full exception handling                      │
│ ✅ raise statements - Custom exceptions                             │
│ ✅ Multiple except clauses - Handle specific errors                 │
│ ✅ finally blocks - Cleanup operations                              │
│ ✅ frappe.throw() - Stop with user message                          │
│ ✅ frappe.log_error() - Silent error logging                        │
│                                                                     │
│ ⚠️ Transaction behavior varies by hook:                            │
│    • validate: throw rolls back entire save                         │
│    • on_update: document already saved!                             │
│    • on_submit: partial rollback possible                           │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────┐
│ 控制器拥有完整的Python能力                                          │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│ ✅ try/except代码块 - 完整的异常处理                                │
│ ✅ raise语句 - 自定义异常                                           │
│ ✅ 多except分支 - 处理特定错误                                       │
│ ✅ finally代码块 - 清理操作                                          │
│ ✅ frappe.throw() - 终止流程并显示用户消息                          │
│ ✅ frappe.log_error() - 静默记录错误日志                              │
│                                                                     │
│ ⚠️ 事务行为因钩子而异:                                              │
│    • validate:throw会回滚整个保存操作                               │
│    • on_update:文档已完成保存!                                     │
│    • on_submit:可能出现部分回滚情况                                 │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

Main Decision: Error Handling by Hook

核心决策:按钩子类型处理错误

┌─────────────────────────────────────────────────────────────────────────┐
│ WHICH LIFECYCLE HOOK ARE YOU IN?                                        │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│ ► validate / before_save                                                │
│   └─► frappe.throw() → Rolls back, document NOT saved                   │
│   └─► try/except → Catch and re-throw or handle gracefully              │
│                                                                         │
│ ► on_update / after_insert                                              │
│   └─► Document already saved! frappe.throw() shows error but saved      │
│   └─► Use try/except + log_error for non-critical operations            │
│   └─► Critical failures: frappe.throw() (shows error, doc is saved)     │
│                                                                         │
│ ► before_submit                                                         │
│   └─► frappe.throw() → Prevents submit, stays draft                     │
│   └─► Last chance for validation before docstatus=1                     │
│                                                                         │
│ ► on_submit                                                             │
│   └─► Document is submitted! throw shows error but docstatus=1          │
│   └─► Critical: throw causes partial state (submitted but failed)       │
│   └─► Better: validate everything in before_submit                      │
│                                                                         │
│ ► on_cancel                                                             │
│   └─► Reverse operations - use try/except for each reversal             │
│   └─► Log errors but try to continue cleanup                            │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────┐
│ 你当前处于哪个生命周期钩子?                                            │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│ ► validate / before_save                                                │
│   └─► frappe.throw() → 执行回滚,文档不会被保存                         │
│   └─► try/except → 捕获异常后重新抛出或优雅处理                        │
│                                                                         │
│ ► on_update / after_insert                                              │
│   └─► 文档已保存!frappe.throw()会显示错误但文档已保存                  │
│   └─► 对非关键操作使用try/except + log_error                          │
│   └─► 严重故障:使用frappe.throw()(显示错误,文档已保存)             │
│                                                                         │
│ ► before_submit                                                         │
│   └─► frappe.throw() → 阻止提交,文档保持草稿状态                     │
│   └─► docstatus=1前的最后验证机会                                     │
│                                                                         │
│ ► on_submit                                                             │
│   └─► 文档已提交!throw会显示错误但docstatus=1                          │
│   └─► 严重问题:throw会导致部分状态异常(已提交但操作失败)             │
│   └─► 更佳方案:在before_submit中完成所有验证                          │
│                                                                         │
│ ► on_cancel                                                             │
│   └─► 反向操作 - 对每个反向步骤使用try/except                         │
│   └─► 记录错误但尽量继续清理操作                                      │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

Error Methods Reference

错误处理方法参考

Quick Reference

快速参考

MethodStops Execution?Rolls Back?User Sees?Use For
frappe.throw()
✅ YESDepends on hookDialogValidation errors
raise Exception
✅ YESDepends on hookError pageInternal errors
frappe.msgprint()
❌ NO❌ NODialogWarnings
frappe.log_error()
❌ NO❌ NOError LogDebug/audit
方法是否终止执行?是否回滚?用户可见?适用场景
frappe.throw()
✅ 是取决于钩子弹窗验证错误
raise Exception
✅ 是取决于钩子错误页面内部错误
frappe.msgprint()
❌ 否❌ 否弹窗警告提示
frappe.log_error()
❌ 否❌ 否错误日志调试/审计

Transaction Rollback by Hook

按钩子划分的事务回滚规则

Hookfrappe.throw() Effect
validate
✅ Full rollback - document NOT saved
before_save
✅ Full rollback - document NOT saved
on_update
⚠️ Document IS saved, error shown
after_insert
⚠️ Document IS saved, error shown
before_submit
✅ Full rollback - stays Draft
on_submit
⚠️ docstatus=1, error shown
before_cancel
✅ Full rollback - stays Submitted
on_cancel
⚠️ docstatus=2, error shown

钩子frappe.throw() 效果
validate
✅ 完全回滚 - 文档未保存
before_save
✅ 完全回滚 - 文档未保存
on_update
⚠️ 文档已保存,仅显示错误
after_insert
⚠️ 文档已保存,仅显示错误
before_submit
✅ 完全回滚 - 保持草稿状态
on_submit
⚠️ docstatus=1,仅显示错误
before_cancel
✅ 完全回滚 - 保持已提交状态
on_cancel
⚠️ docstatus=2,仅显示错误

Error Handling Patterns

错误处理模式

Pattern 1: Validation with Error Collection

模式1:收集所有验证错误后抛出

python
def validate(self):
    """Collect all errors before throwing."""
    errors = []
    
    # Required fields
    if not self.customer:
        errors.append(_("Customer is required"))
    
    if not self.items:
        errors.append(_("At least one item is required"))
    
    # Business rules
    if self.discount_percent > 50:
        errors.append(_("Discount cannot exceed 50%"))
    
    # Child table validation
    for idx, item in enumerate(self.items, 1):
        if not item.item_code:
            errors.append(_("Row {0}: Item Code is required").format(idx))
        if (item.qty or 0) <= 0:
            errors.append(_("Row {0}: Quantity must be positive").format(idx))
    
    # Throw all errors at once
    if errors:
        frappe.throw("<br>".join(errors), title=_("Validation Error"))
python
def validate(self):
    """收集所有错误后再抛出。"""
    errors = []
    
    # 必填字段检查
    if not self.customer:
        errors.append(_("客户为必填项"))
    
    if not self.items:
        errors.append(_("至少需要一个项目"))
    
    # 业务规则检查
    if self.discount_percent > 50:
        errors.append(_("折扣不能超过50%"))
    
    # 子表验证
    for idx, item in enumerate(self.items, 1):
        if not item.item_code:
            errors.append(_("第{0}行:项目编码为必填项").format(idx))
        if (item.qty or 0) <= 0:
            errors.append(_("第{0}行:数量必须为正数").format(idx))
    
    # 一次性抛出所有错误
    if errors:
        frappe.throw("<br>".join(errors), title=_("验证错误"))

Pattern 2: External API Call with Fallback

模式2:带降级方案的外部API调用

python
def validate(self):
    """Call external API with error handling."""
    if self.requires_credit_check:
        try:
            result = self.check_credit_external()
            self.credit_score = result.get("score", 0)
        except requests.Timeout:
            # Timeout - use cached value
            frappe.msgprint(
                _("Credit check timed out. Using cached value."),
                indicator="orange"
            )
            self.credit_score = self.get_cached_credit_score()
        except requests.RequestException as e:
            # API error - log and continue with warning
            frappe.log_error(
                f"Credit check failed: {str(e)}",
                "External API Error"
            )
            frappe.msgprint(
                _("Credit check unavailable. Please verify manually."),
                indicator="orange"
            )
            self.credit_check_pending = 1
        except Exception as e:
            # Unexpected error - log and re-raise
            frappe.log_error(frappe.get_traceback(), "Credit Check Error")
            frappe.throw(_("Credit check failed. Please try again."))
python
def validate(self):
    """带错误处理的外部API调用。"""
    if self.requires_credit_check:
        try:
            result = self.check_credit_external()
            self.credit_score = result.get("score", 0)
        except requests.Timeout:
            # 超时 - 使用缓存值
            frappe.msgprint(
                _("信用检查超时,将使用缓存值。"),
                indicator="orange"
            )
            self.credit_score = self.get_cached_credit_score()
        except requests.RequestException as e:
            # API错误 - 记录日志并带警告继续
            frappe.log_error(
                f"信用检查失败:{str(e)}",
                "外部API错误"
            )
            frappe.msgprint(
                _("信用检查不可用,请手动验证。"),
                indicator="orange"
            )
            self.credit_check_pending = 1
        except Exception as e:
            # 意外错误 - 记录日志并重抛
            frappe.log_error(frappe.get_traceback(), "信用检查错误")
            frappe.throw(_("信用检查失败,请重试。"))

Pattern 3: Safe Post-Save Operations

模式3:安全的保存后操作

python
def on_update(self):
    """Handle post-save operations safely."""
    # Critical operation - throw on failure
    self.update_linked_documents()
    
    # Non-critical operations - log errors, don't throw
    try:
        self.send_notification()
    except Exception:
        frappe.log_error(
            frappe.get_traceback(),
            f"Notification failed for {self.name}"
        )
    
    try:
        self.sync_to_external_system()
    except Exception:
        frappe.log_error(
            frappe.get_traceback(),
            f"External sync failed for {self.name}"
        )
        # Queue for retry
        frappe.enqueue(
            "myapp.tasks.retry_sync",
            doctype=self.doctype,
            name=self.name,
            queue="short"
        )
python
def on_update(self):
    """安全处理保存后操作。"""
    # 关键操作 - 失败则抛出错误
    self.update_linked_documents()
    
    # 非关键操作 - 记录错误但不抛出
    try:
        self.send_notification()
    except Exception:
        frappe.log_error(
            frappe.get_traceback(),
            f"{self.name}的通知发送失败"
        )
    
    try:
        self.sync_to_external_system()
    except Exception:
        frappe.log_error(
            frappe.get_traceback(),
            f"{self.name}的外部系统同步失败"
        )
        # 加入队列重试
        frappe.enqueue(
            "myapp.tasks.retry_sync",
            doctype=self.doctype,
            name=self.name,
            queue="short"
        )

Pattern 4: Submittable Document Error Handling

模式4:可提交文档的错误处理

python
def before_submit(self):
    """All validations that must pass before submit."""
    # Validate everything here - last chance to abort cleanly
    if not self.items:
        frappe.throw(_("Cannot submit without items"))
    
    if self.grand_total <= 0:
        frappe.throw(_("Total must be greater than zero"))
    
    # Check stock availability
    for item in self.items:
        available = get_stock_balance(item.item_code, item.warehouse)
        if available < item.qty:
            frappe.throw(
                _("Row {0}: Insufficient stock for {1}. Available: {2}").format(
                    item.idx, item.item_code, available
                )
            )

def on_submit(self):
    """Post-submit actions - document is already submitted!"""
    # These operations should not fail if before_submit passed
    try:
        self.create_stock_ledger_entries()
    except Exception as e:
        # CRITICAL: Document is submitted but entries failed!
        frappe.log_error(frappe.get_traceback(), "Stock Ledger Error")
        frappe.throw(
            _("Stock entries failed. Please cancel and retry. Error: {0}").format(str(e))
        )
    
    try:
        self.create_gl_entries()
    except Exception as e:
        # Rollback stock entries if GL fails
        self.reverse_stock_ledger_entries()
        frappe.log_error(frappe.get_traceback(), "GL Entry Error")
        frappe.throw(_("Accounting entries failed. Stock entries reversed."))
python
def before_submit(self):
    """提交前必须通过的所有验证。"""
    # 在此处完成所有验证 - 这是干净终止的最后机会
    if not self.items:
        frappe.throw(_("无项目无法提交"))
    
    if self.grand_total <= 0:
        frappe.throw(_("总金额必须大于0"))
    
    # 检查库存可用性
    for item in self.items:
        available = get_stock_balance(item.item_code, item.warehouse)
        if available < item.qty:
            frappe.throw(
                _("第{0}行:{1}库存不足。可用库存:{2}").format(
                    item.idx, item.item_code, available
                )
            )

def on_submit(self):
    """提交后操作 - 文档已完成提交!"""
    # 如果before_submit验证通过,这些操作不应失败
    try:
        self.create_stock_ledger_entries()
    except Exception as e:
        # 严重问题:文档已提交但库存分录失败!
        frappe.log_error(frappe.get_traceback(), "库存台账错误")
        frappe.throw(
            _("库存分录失败,请取消后重试。错误:{0}").format(str(e))
        )
    
    try:
        self.create_gl_entries()
    except Exception as e:
        # 如果总账分录失败,回滚库存分录
        self.reverse_stock_ledger_entries()
        frappe.log_error(frappe.get_traceback(), "总账分录错误")
        frappe.throw(_("会计分录失败,库存分录已回滚。"))

Pattern 5: Cancel with Cleanup

模式5:带清理操作的取消流程

python
def before_cancel(self):
    """Validate cancel is allowed."""
    # Check for linked documents
    linked_invoices = frappe.get_all(
        "Sales Invoice Item",
        filters={"sales_order": self.name, "docstatus": 1},
        pluck="parent"
    )
    
    if linked_invoices:
        frappe.throw(
            _("Cannot cancel. Linked invoices exist: {0}").format(
                ", ".join(linked_invoices)
            )
        )

def on_cancel(self):
    """Reverse operations - try to complete all cleanup."""
    errors = []
    
    # Reverse stock
    try:
        self.reverse_stock_ledger_entries()
    except Exception as e:
        errors.append(f"Stock reversal: {str(e)}")
        frappe.log_error(frappe.get_traceback(), "Stock Reversal Error")
    
    # Reverse GL
    try:
        self.reverse_gl_entries()
    except Exception as e:
        errors.append(f"GL reversal: {str(e)}")
        frappe.log_error(frappe.get_traceback(), "GL Reversal Error")
    
    # Update linked docs
    try:
        self.update_linked_on_cancel()
    except Exception as e:
        errors.append(f"Linked docs: {str(e)}")
        frappe.log_error(frappe.get_traceback(), "Linked Doc Update Error")
    
    # Report any errors but don't prevent cancel
    if errors:
        frappe.msgprint(
            _("Cancel completed with errors:<br>{0}").format("<br>".join(errors)),
            title=_("Warning"),
            indicator="orange"
        )
python
def before_cancel(self):
    """验证是否允许取消。"""
    # 检查关联文档
    linked_invoices = frappe.get_all(
        "Sales Invoice Item",
        filters={"sales_order": self.name, "docstatus": 1},
        pluck="parent"
    )
    
    if linked_invoices:
        frappe.throw(
            _("无法取消,存在关联发票:{0}").format(
                ", ".join(linked_invoices)
            )
        )

def on_cancel(self):
    """反向操作 - 尽量完成所有清理。"""
    errors = []
    
    # 回滚库存
    try:
        self.reverse_stock_ledger_entries()
    except Exception as e:
        errors.append(f"库存回滚:{str(e)}")
        frappe.log_error(frappe.get_traceback(), "库存回滚错误")
    
    # 回滚总账
    try:
        self.reverse_gl_entries()
    except Exception as e:
        errors.append(f"总账回滚:{str(e)}")
        frappe.log_error(frappe.get_traceback(), "总账回滚错误")
    
    # 更新关联文档
    try:
        self.update_linked_on_cancel()
    except Exception as e:
        errors.append(f"关联文档:{str(e)}")
        frappe.log_error(frappe.get_traceback(), "关联文档更新错误")
    
    # 报告错误但不阻止取消
    if errors:
        frappe.msgprint(
            _("取消操作已完成,但存在以下错误:<br>{0}").format("<br>".join(errors)),
            title=_("警告"),
            indicator="orange"
        )

Pattern 6: Database Operation Error Handling

模式6:数据库操作的错误处理

python
def validate(self):
    """Handle database errors gracefully."""
    try:
        # Check for duplicates
        existing = frappe.db.exists(
            "Customer Contract",
            {"customer": self.customer, "status": "Active", "name": ["!=", self.name]}
        )
        if existing:
            frappe.throw(_("Active contract already exists for this customer"))
            
    except frappe.db.InternalError as e:
        # Database error - log and show user-friendly message
        frappe.log_error(frappe.get_traceback(), "Database Error")
        frappe.throw(_("Database error. Please try again or contact support."))
See:
references/patterns.md
for more error handling patterns.

python
def validate(self):
    """优雅处理数据库错误。"""
    try:
        # 检查重复项
        existing = frappe.db.exists(
            "Customer Contract",
            {"customer": self.customer, "status": "Active", "name": ["!=", self.name]}
        )
        if existing:
            frappe.throw(_("该客户已存在有效的合同"))
            
    except frappe.db.InternalError as e:
        # 数据库错误 - 记录日志并显示友好提示
        frappe.log_error(frappe.get_traceback(), "数据库错误")
        frappe.throw(_("数据库错误,请重试或联系支持人员。"))
参考:更多错误处理模式请查看
references/patterns.md

Transaction Management

事务管理

Understanding Transactions

事务理解

python
undefined
python
undefined

Frappe wraps each request in a transaction

Frappe会为每个请求自动包裹事务

- On success: auto-commit

- 成功时:自动提交

- On exception: auto-rollback

- 异常时:自动回滚

def validate(self): # All these changes are in ONE transaction self.calculate_totals() frappe.db.set_value("Counter", "main", "count", 100)
if error_condition:
    frappe.throw("Error")  # EVERYTHING rolls back
def on_update(self): # Document save is already committed! # New changes here are in a NEW transaction frappe.db.set_value("Other", "doc", "field", "value")
if error_condition:
    frappe.throw("Error")  # Only on_update changes roll back
    # The document itself is already saved!
undefined
def validate(self): # 所有这些修改都在同一个事务中 self.calculate_totals() frappe.db.set_value("Counter", "main", "count", 100)
if error_condition:
    frappe.throw("错误")  # 所有修改都会回滚
def on_update(self): # 文档保存已完成提交! # 此处的新修改属于新事务 frappe.db.set_value("Other", "doc", "field", "value")
if error_condition:
    frappe.throw("错误")  # 仅回滚on_update中的修改
    # 文档本身已保存!
undefined

Manual Savepoints (Advanced)

手动保存点(高级用法)

python
def on_submit(self):
    """Use savepoints for partial rollback."""
    # Create savepoint before risky operation
    frappe.db.savepoint("before_stock")
    
    try:
        self.create_stock_entries()
    except Exception:
        # Rollback only stock entries
        frappe.db.rollback(save_point="before_stock")
        frappe.log_error(frappe.get_traceback(), "Stock Entry Error")
        frappe.throw(_("Stock entries failed"))
    
    frappe.db.savepoint("before_gl")
    
    try:
        self.create_gl_entries()
    except Exception:
        frappe.db.rollback(save_point="before_gl")
        frappe.log_error(frappe.get_traceback(), "GL Entry Error")
        frappe.throw(_("GL entries failed"))

python
def on_submit(self):
    """使用保存点实现部分回滚。"""
    # 风险操作前创建保存点
    frappe.db.savepoint("before_stock")
    
    try:
        self.create_stock_entries()
    except Exception:
        # 仅回滚库存操作
        frappe.db.rollback(save_point="before_stock")
        frappe.log_error(frappe.get_traceback(), "库存分录错误")
        frappe.throw(_("库存分录失败"))
    
    frappe.db.savepoint("before_gl")
    
    try:
        self.create_gl_entries()
    except Exception:
        frappe.db.rollback(save_point="before_gl")
        frappe.log_error(frappe.get_traceback(), "总账分录错误")
        frappe.throw(_("总账分录失败"))

Critical Rules

核心规则

✅ ALWAYS

✅ 必须遵守

  1. Collect multiple validation errors - Better UX than one at a time
  2. Use try/except around external calls - APIs, file I/O, network
  3. Log unexpected errors -
    frappe.log_error(frappe.get_traceback())
  4. Call super() in overridden methods - Preserve parent behavior
  5. Validate in before_submit - Last clean abort point for submittables
  6. Use _() for error messages - Enable translation
  1. 收集多个验证错误 - 一次性显示比逐个显示更优的用户体验
  2. 外部调用包裹try/except - 包括API、文件I/O、网络操作
  3. 记录意外错误 - 使用
    frappe.log_error(frappe.get_traceback())
  4. 重写方法时调用super() - 保留父类行为
  5. 在before_submit中完成验证 - 可提交文档的最后干净终止点
  6. 错误消息使用_()包裹 - 支持多语言翻译

❌ NEVER

❌ 严禁操作

  1. Don't call frappe.db.commit() - Framework handles transactions
  2. Don't swallow errors silently - Always log unexpected exceptions
  3. Don't assume on_update can rollback doc - It's already saved
  4. Don't put critical logic in on_submit - Validate in before_submit
  5. Don't ignore return values - Check for None/empty results

  1. 不要调用frappe.db.commit() - 框架会自动处理事务
  2. 不要静默吞掉错误 - 意外异常必须记录日志
  3. 不要假设on_update可以回滚文档 - 文档已保存
  4. 不要在on_submit中放置关键逻辑 - 应在before_submit中验证
  5. 不要忽略返回值 - 检查None/空结果

Quick Reference: Exception Handling

快速参考:异常处理

python
undefined
python
undefined

Catch specific exceptions first, general last

先捕获特定异常,最后捕获通用异常

try: result = risky_operation() except frappe.ValidationError: # Re-raise validation errors raise except frappe.DoesNotExistError: # Handle missing document frappe.throw(("Referenced document not found")) except requests.Timeout: # Handle timeout specifically frappe.msgprint(("Operation timed out"), indicator="orange") except Exception as e: # Log and handle unexpected errors frappe.log_error(frappe.get_traceback(), "Unexpected Error") frappe.throw(_("An error occurred: {0}").format(str(e)))

---
try: result = risky_operation() except frappe.ValidationError: # 重抛验证错误 raise except frappe.DoesNotExistError: # 处理文档不存在的情况 frappe.throw(("引用的文档不存在")) except requests.Timeout: # 专门处理超时 frappe.msgprint(("操作超时"), indicator="orange") except Exception as e: # 记录并处理意外错误 frappe.log_error(frappe.get_traceback(), "意外错误") frappe.throw(_("发生错误:{0}").format(str(e)))

---

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-controllers
    - Controller syntax
  • erpnext-impl-controllers
    - Implementation workflows
  • erpnext-errors-serverscripts
    - Server Script error handling (sandbox)
  • erpnext-errors-hooks
    - Hook error handling
  • erpnext-syntax-controllers
    - 控制器语法
  • erpnext-impl-controllers
    - 实现工作流
  • erpnext-errors-serverscripts
    - 服务器脚本错误处理(沙箱环境)
  • erpnext-errors-hooks
    - 钩子错误处理