erpnext-errors-hooks

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

ERPNext Hooks - Error Handling

ERPNext Hooks - 错误处理

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

本技能涵盖hooks.py配置的错误处理模式。语法相关内容请参考
erpnext-syntax-hooks
。实现工作流相关内容请参考
erpnext-impl-hooks
版本:兼容v14/v15/v16

Hooks Error Handling Overview

Hooks错误处理概述

┌─────────────────────────────────────────────────────────────────────┐
│ HOOKS HAVE UNIQUE ERROR HANDLING CHARACTERISTICS                    │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│ ✅ Full Python power (try/except, raise)                            │
│ ⚠️ Multiple handlers in chain - one failure affects others         │
│ ⚠️ Some hooks are silent (scheduler, permission_query)             │
│ ⚠️ Transaction behavior varies by hook type                        │
│                                                                     │
│ Key differences from controllers:                                   │
│ • doc_events runs AFTER controller methods                          │
│ • Multiple apps can register handlers (order matters!)              │
│ • Scheduler has NO user feedback - logging is critical              │
│ • Permission hooks should NEVER throw errors                        │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────┐
│ HOOKS具有独特的错误处理特性                                        │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│ ✅ 拥有完整的Python能力(try/except、raise)                        │
│ ⚠️ 链式调用中的多个处理器 - 一个失败会影响其他处理器                 │
│ ⚠️ 部分钩子无提示(调度器、permission_query)                        │
│ ⚠️ 事务行为因钩子类型而异                                          │
│                                                                     │
│ 与控制器的主要区别:                                               │
│ • doc_events在控制器方法之后运行                                    │
│ • 多个应用可注册处理器(执行顺序很重要!)                          │
│ • 调度器无用户反馈 - 日志记录至关重要                              │
│ • 权限钩子绝不能抛出错误                                          │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

Main Decision: Error Handling by Hook Type

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

┌─────────────────────────────────────────────────────────────────────────┐
│ WHICH HOOK TYPE ARE YOU USING?                                          │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│ ► doc_events (validate, on_update, on_submit, etc.)                     │
│   └─► Same as controllers: frappe.throw() rolls back in validate        │
│   └─► Multiple handlers: first error stops chain                        │
│   └─► Isolate non-critical operations in try/except                     │
│                                                                         │
│ ► scheduler_events (daily, hourly, cron)                                │
│   └─► NO user feedback - frappe.log_error() is essential                │
│   └─► ALWAYS use try/except around operations                           │
│   └─► MUST call frappe.db.commit() manually                             │
│                                                                         │
│ ► permission_query_conditions                                           │
│   └─► NEVER throw errors - return empty string on error                 │
│   └─► Silent failures break list views                                  │
│   └─► Log errors but return safe fallback                               │
│                                                                         │
│ ► has_permission                                                        │
│   └─► NEVER throw errors - return False on error                        │
│   └─► Return None to defer to default permission                        │
│                                                                         │
│ ► override_doctype_class / extend_doctype_class                         │
│   └─► ALWAYS call super() in try/except                                 │
│   └─► Parent errors should usually propagate                            │
│                                                                         │
│ ► extend_bootinfo                                                       │
│   └─► Errors break page load entirely!                                  │
│   └─► ALWAYS wrap in try/except with fallback                           │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────┐
│ 你正在使用哪种钩子类型?                                              │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│ ► doc_events(validate、on_update、on_submit等)                         │
│   └─► 与控制器相同:frappe.throw()会在validate阶段触发完全回滚        │
│   └─► 多处理器链式调用:第一个错误会终止后续调用                        │
│   └─► 将非关键操作隔离在try/except中                                   │
│                                                                         │
│ ► scheduler_events(daily、hourly、cron)                                │
│   └─► 无用户反馈 - frappe.log_error()必不可少                            │
│   └─► 务必在操作外层使用try/except                                      │
│   └─► 必须手动调用frappe.db.commit()                                    │
│                                                                         │
│ ► permission_query_conditions                                           │
│   └─► 绝不能抛出错误 - 出错时返回空字符串                                │
│   └─► 无提示失败会破坏列表视图                                          │
│   └─► 记录错误但返回安全的回退值                                       │
│                                                                         │
│ ► has_permission                                                        │
│   └─► 绝不能抛出错误 - 出错时返回False                                  │
│   └─► 返回None以 defer 到默认权限系统                                    │
│                                                                         │
│ ► override_doctype_class / extend_doctype_class                         │
│   └─► 务必在try/except中调用super()                                     │
│   └─► 父类错误通常应向上传播                                            │
│                                                                         │
│ ► extend_bootinfo                                                       │
│   └─► 错误会完全中断页面加载!                                          │
│   └─► 务必用try/except包裹并设置回退值                                   │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

doc_events Error Handling

doc_events错误处理

Transaction Behavior (Same as Controllers)

事务行为(与控制器相同)

Eventfrappe.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
on_submit
⚠️ docstatus=1, error shown
on_cancel
⚠️ docstatus=2, error shown
事件frappe.throw() 效果
validate
✅ 完全回滚 - 文档不保存
before_save
✅ 完全回滚 - 文档不保存
on_update
⚠️ 文档已保存,显示错误
after_insert
⚠️ 文档已保存,显示错误
on_submit
⚠️ docstatus=1,显示错误
on_cancel
⚠️ docstatus=2,显示错误

Multiple Handler Chain

多处理器链式调用

python
undefined
python
undefined

hooks.py - Multiple apps can register handlers

hooks.py - Multiple apps can register handlers

App A

App A

doc_events = { "Sales Invoice": { "validate": "app_a.events.validate_si" # Runs first } }
doc_events = { "Sales Invoice": { "validate": "app_a.events.validate_si" # Runs first } }

App B

App B

doc_events = { "Sales Invoice": { "validate": "app_b.events.validate_si" # Runs second } }
doc_events = { "Sales Invoice": { "validate": "app_b.events.validate_si" # Runs second } }

If App A throws error, App B's handler NEVER runs!

If App A throws error, App B's handler NEVER runs!

undefined
undefined

Pattern: Validate Handler

模式:验证处理器

python
undefined
python
undefined

myapp/events/sales_invoice.py

myapp/events/sales_invoice.py

import frappe from frappe import _
def validate(doc, method=None): """Validate handler with proper error handling.""" errors = []
# Collect validation errors
if doc.grand_total < 0:
    errors.append(_("Total cannot be negative"))

if doc.custom_field and not doc.customer:
    errors.append(_("Customer required when custom field is set"))

# Throw all at once
if errors:
    frappe.throw("<br>".join(errors))
undefined
import frappe from frappe import _
def validate(doc, method=None): """Validate handler with proper error handling.""" errors = []
# Collect validation errors
if doc.grand_total < 0:
    errors.append(_("Total cannot be negative"))

if doc.custom_field and not doc.customer:
    errors.append(_("Customer required when custom field is set"))

# Throw all at once
if errors:
    frappe.throw("<br>".join(errors))
undefined

Pattern: on_update Handler (Isolated Operations)

模式:on_update处理器(隔离操作)

python
def on_update(doc, method=None):
    """Post-save handler with isolated operations."""
    # Critical operation - let errors propagate
    update_linked_records(doc)
    
    # Non-critical operations - isolate errors
    try:
        send_notification(doc)
    except Exception:
        frappe.log_error(
            frappe.get_traceback(),
            f"Notification failed for {doc.name}"
        )
    
    try:
        sync_to_external(doc)
    except Exception:
        frappe.log_error(
            frappe.get_traceback(),
            f"External sync failed for {doc.name}"
        )

python
def on_update(doc, method=None):
    """Post-save handler with isolated operations."""
    # Critical operation - let errors propagate
    update_linked_records(doc)
    
    # Non-critical operations - isolate errors
    try:
        send_notification(doc)
    except Exception:
        frappe.log_error(
            frappe.get_traceback(),
            f"Notification failed for {doc.name}"
        )
    
    try:
        sync_to_external(doc)
    except Exception:
        frappe.log_error(
            frappe.get_traceback(),
            f"External sync failed for {doc.name}"
        )

scheduler_events Error Handling

scheduler_events错误处理

Critical: No User Feedback!

关键提示:无用户反馈!

┌─────────────────────────────────────────────────────────────────────┐
│ ⚠️  SCHEDULER TASKS HAVE NO USER - LOGGING IS ESSENTIAL             │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│ • No one sees frappe.throw() - task just fails silently             │
│ • No automatic email on failure (unless configured)                 │
│ • frappe.log_error() is your ONLY debugging tool                    │
│ • Always commit changes manually                                    │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ ⚠️  调度器任务无用户交互 - 日志记录至关重要                          │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│ • 无人会看到frappe.throw() - 任务只会无提示失败                     │
│ • 失败时无自动邮件通知(除非配置)                                   │
│ • frappe.log_error()是你唯一的调试工具                              │
│ • 务必手动提交变更                                                  │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

Pattern: Scheduler Task with Error Handling

模式:带错误处理的调度器任务

python
undefined
python
undefined

myapp/tasks.py

myapp/tasks.py

import frappe
def daily_sync(): """Daily sync task with comprehensive error handling.""" results = { "processed": 0, "errors": [] }
try:
    # Get records to process (ALWAYS with limit!)
    records = frappe.get_all(
        "Sales Invoice",
        filters={"sync_status": "Pending"},
        limit=500
    )
    
    for record in records:
        try:
            process_record(record.name)
            results["processed"] += 1
        except Exception as e:
            results["errors"].append(f"{record.name}: {str(e)}")
            frappe.log_error(
                frappe.get_traceback(),
                f"Sync error: {record.name}"
            )
    
    # REQUIRED: Commit changes
    frappe.db.commit()
    
except Exception as e:
    # Log fatal errors
    frappe.log_error(
        frappe.get_traceback(),
        "Daily Sync Fatal Error"
    )
    return

# Log summary
if results["errors"]:
    summary = f"Processed: {results['processed']}, Errors: {len(results['errors'])}"
    frappe.log_error(
        summary + "\n\n" + "\n".join(results["errors"][:50]),
        "Daily Sync Summary"
    )
undefined
import frappe
def daily_sync(): """Daily sync task with comprehensive error handling.""" results = { "processed": 0, "errors": [] }
try:
    # Get records to process (ALWAYS with limit!)
    records = frappe.get_all(
        "Sales Invoice",
        filters={"sync_status": "Pending"},
        limit=500
    )
    
    for record in records:
        try:
            process_record(record.name)
            results["processed"] += 1
        except Exception as e:
            results["errors"].append(f"{record.name}: {str(e)}")
            frappe.log_error(
                frappe.get_traceback(),
                f"Sync error: {record.name}"
            )
    
    # REQUIRED: Commit changes
    frappe.db.commit()
    
except Exception as e:
    # Log fatal errors
    frappe.log_error(
        frappe.get_traceback(),
        "Daily Sync Fatal Error"
    )
    return

# Log summary
if results["errors"]:
    summary = f"Processed: {results['processed']}, Errors: {len(results['errors'])}"
    frappe.log_error(
        summary + "\n\n" + "\n".join(results["errors"][:50]),
        "Daily Sync Summary"
    )
undefined

Pattern: Scheduler with Batch Commits

模式:带批量提交的调度器

python
def process_large_dataset():
    """Process large dataset with periodic commits."""
    BATCH_SIZE = 100
    
    try:
        records = frappe.get_all("Item", limit=5000)
        total = len(records)
        
        for i in range(0, total, BATCH_SIZE):
            batch = records[i:i + BATCH_SIZE]
            
            for record in batch:
                try:
                    update_item(record.name)
                except Exception:
                    frappe.log_error(
                        frappe.get_traceback(),
                        f"Item update error: {record.name}"
                    )
            
            # Commit after each batch
            frappe.db.commit()
            
    except Exception:
        frappe.log_error(frappe.get_traceback(), "Batch Processing Error")

python
def process_large_dataset():
    """Process large dataset with periodic commits."""
    BATCH_SIZE = 100
    
    try:
        records = frappe.get_all("Item", limit=5000)
        total = len(records)
        
        for i in range(0, total, BATCH_SIZE):
            batch = records[i:i + BATCH_SIZE]
            
            for record in batch:
                try:
                    update_item(record.name)
                except Exception:
                    frappe.log_error(
                        frappe.get_traceback(),
                        f"Item update error: {record.name}"
                    )
            
            # Commit after each batch
            frappe.db.commit()
            
    except Exception:
        frappe.log_error(frappe.get_traceback(), "Batch Processing Error")

Permission Hooks Error Handling

权限钩子错误处理

permission_query_conditions - NEVER Throw!

permission_query_conditions - 绝不能抛出错误!

python
undefined
python
undefined

❌ WRONG - Breaks list view entirely!

❌ WRONG - Breaks list view entirely!

def query_conditions(user): if not user: frappe.throw("User required") # DON'T DO THIS! return f"owner = '{user}'"
def query_conditions(user): if not user: frappe.throw("User required") # DON'T DO THIS! return f"owner = '{user}'"

✅ CORRECT - Return safe fallback

✅ CORRECT - Return safe fallback

def query_conditions(user): """Permission query with error handling.""" try: if not user: user = frappe.session.user
    if "System Manager" in frappe.get_roles(user):
        return ""  # No restrictions
    
    return f"`tabSales Invoice`.owner = {frappe.db.escape(user)}"
    
except Exception:
    frappe.log_error(
        frappe.get_traceback(),
        "Permission Query Error"
    )
    # Safe fallback - restrict to own records
    return f"`tabSales Invoice`.owner = {frappe.db.escape(frappe.session.user)}"
undefined
def query_conditions(user): """Permission query with error handling.""" try: if not user: user = frappe.session.user
    if "System Manager" in frappe.get_roles(user):
        return ""  # No restrictions
    
    return f"`tabSales Invoice`.owner = {frappe.db.escape(user)}"
    
except Exception:
    frappe.log_error(
        frappe.get_traceback(),
        "Permission Query Error"
    )
    # Safe fallback - restrict to own records
    return f"`tabSales Invoice`.owner = {frappe.db.escape(frappe.session.user)}"
undefined

has_permission - NEVER Throw!

has_permission - 绝不能抛出错误!

python
undefined
python
undefined

❌ WRONG - Breaks document access!

❌ WRONG - Breaks document access!

def has_permission(doc, user=None, permission_type=None): if doc.status == "Locked": frappe.throw("Document is locked") # DON'T DO THIS!
def has_permission(doc, user=None, permission_type=None): if doc.status == "Locked": frappe.throw("Document is locked") # DON'T DO THIS!

✅ CORRECT - Return boolean or None

✅ CORRECT - Return boolean or None

def has_permission(doc, user=None, permission_type=None): """Document permission check with error handling.""" try: user = user or frappe.session.user
    # Deny access to locked documents
    if doc.status == "Locked" and permission_type == "write":
        return False
    
    # Custom logic
    if permission_type == "delete":
        if doc.has_linked_records():
            return False
    
    # Return None to defer to default permission system
    return None
    
except Exception:
    frappe.log_error(
        frappe.get_traceback(),
        f"Permission check error: {doc.name}"
    )
    # Safe fallback - defer to default
    return None

---
def has_permission(doc, user=None, permission_type=None): """Document permission check with error handling.""" try: user = user or frappe.session.user
    # Deny access to locked documents
    if doc.status == "Locked" and permission_type == "write":
        return False
    
    # Custom logic
    if permission_type == "delete":
        if doc.has_linked_records():
            return False
    
    # Return None to defer to default permission system
    return None
    
except Exception:
    frappe.log_error(
        frappe.get_traceback(),
        f"Permission check error: {doc.name}"
    )
    # Safe fallback - defer to default
    return None

---

Override Hooks Error Handling

覆盖钩子错误处理

override_doctype_class

override_doctype_class

python
undefined
python
undefined

myapp/overrides.py

myapp/overrides.py

from erpnext.selling.doctype.sales_order.sales_order import SalesOrder import frappe from frappe import _
class CustomSalesOrder(SalesOrder): def validate(self): """Override with proper error handling.""" # ALWAYS call parent first in try/except try: super().validate() except frappe.ValidationError: # Re-raise validation errors raise except Exception as e: frappe.log_error(frappe.get_traceback(), "Parent Validate Error") raise
    # Custom validation
    self.custom_validate()

def custom_validate(self):
    if self.custom_approval_required and not self.custom_approved:
        frappe.throw(_("Approval required before saving"))
undefined
from erpnext.selling.doctype.sales_order.sales_order import SalesOrder import frappe from frappe import _
class CustomSalesOrder(SalesOrder): def validate(self): """Override with proper error handling.""" # ALWAYS call parent first in try/except try: super().validate() except frappe.ValidationError: # Re-raise validation errors raise except Exception as e: frappe.log_error(frappe.get_traceback(), "Parent Validate Error") raise
    # Custom validation
    self.custom_validate()

def custom_validate(self):
    if self.custom_approval_required and not self.custom_approved:
        frappe.throw(_("Approval required before saving"))
undefined

extend_doctype_class (V16+)

extend_doctype_class(V16+)

python
undefined
python
undefined

myapp/extends.py

myapp/extends.py

import frappe from frappe import _
class SalesOrderExtend: """Extension class - only add new methods."""
def custom_approval_check(self):
    """New method with error handling."""
    try:
        if not self.custom_approver:
            frappe.throw(_("Approver not set"))
        
        approver = frappe.get_doc("User", self.custom_approver)
        if not approver.enabled:
            frappe.throw(_("Approver is disabled"))
            
    except frappe.DoesNotExistError:
        frappe.throw(_("Approver not found"))

---
import frappe from frappe import _
class SalesOrderExtend: """Extension class - only add new methods."""
def custom_approval_check(self):
    """New method with error handling."""
    try:
        if not self.custom_approver:
            frappe.throw(_("Approver not set"))
        
        approver = frappe.get_doc("User", self.custom_approver)
        if not approver.enabled:
            frappe.throw(_("Approver is disabled"))
            
    except frappe.DoesNotExistError:
        frappe.throw(_("Approver not found"))

---

extend_bootinfo Error Handling

extend_bootinfo错误处理

Critical: Errors Break Page Load!

关键提示:错误会中断页面加载!

python
undefined
python
undefined

❌ WRONG - Unhandled error breaks desk entirely!

❌ WRONG - Unhandled error breaks desk entirely!

def extend_boot(bootinfo): settings = frappe.get_single("My Settings") # What if it doesn't exist? bootinfo.my_config = settings.config
def extend_boot(bootinfo): settings = frappe.get_single("My Settings") # What if it doesn't exist? bootinfo.my_config = settings.config

✅ CORRECT - Always handle errors

✅ CORRECT - Always handle errors

def extend_boot(bootinfo): """Extend bootinfo with error handling.""" try: if frappe.db.exists("My Settings", "My Settings"): settings = frappe.get_single("My Settings") bootinfo.my_config = settings.config or {} else: bootinfo.my_config = {}
except Exception:
    frappe.log_error(
        frappe.get_traceback(),
        "Bootinfo Extension Error"
    )
    # Safe fallback
    bootinfo.my_config = {}

---
def extend_boot(bootinfo): """Extend bootinfo with error handling.""" try: if frappe.db.exists("My Settings", "My Settings"): settings = frappe.get_single("My Settings") bootinfo.my_config = settings.config or {} else: bootinfo.my_config = {}
except Exception:
    frappe.log_error(
        frappe.get_traceback(),
        "Bootinfo Extension Error"
    )
    # Safe fallback
    bootinfo.my_config = {}

---

Critical Rules

核心规则

✅ ALWAYS

✅ 务必遵守

  1. Use try/except in scheduler tasks - No user feedback otherwise
  2. Call frappe.db.commit() in scheduler - Changes aren't auto-saved
  3. Return safe fallbacks in permission hooks - Never throw
  4. Call super() in override classes - Preserve parent behavior
  5. Log errors with context - Include document name, operation
  6. Wrap extend_bootinfo in try/except - Errors break page load
  1. 在调度器任务中使用try/except - 否则无用户反馈
  2. 在调度器中调用frappe.db.commit() - 变更不会自动保存
  3. 在权限钩子中返回安全回退值 - 绝不能抛出错误
  4. 在覆盖类中调用super() - 保留父类行为
  5. 记录带上下文的错误 - 包含文档名称、操作
  6. 用try/except包裹extend_bootinfo - 错误会中断页面加载

❌ NEVER

❌ 绝不能做

  1. Don't throw in permission_query_conditions - Breaks list views
  2. Don't throw in has_permission - Breaks document access
  3. Don't assume single handler - Multiple apps can register
  4. Don't commit in doc_events - Framework handles transactions
  5. Don't ignore scheduler errors - They fail silently

  1. 不要在permission_query_conditions中抛出错误 - 会破坏列表视图
  2. 不要在has_permission中抛出错误 - 会破坏文档访问
  3. 不要假设只有单个处理器 - 多个应用可注册处理器
  4. 不要在doc_events中提交事务 - 框架会处理事务
  5. 不要忽略调度器错误 - 它们会无提示失败

Quick Reference: Error Handling by Hook

速查:按钩子类型处理错误

Hook TypeCan Throw?Commit?Key Pattern
doc_events (validate)✅ YES❌ NOCollect errors, throw once
doc_events (on_update)⚠️ Careful❌ NOIsolate non-critical ops
scheduler_events❌ Pointless✅ YESTry/except + log_error
permission_query_conditions❌ NEVER❌ NOReturn "" on error
has_permission❌ NEVER❌ NOReturn None on error
extend_bootinfo❌ NEVER❌ NOTry/except + fallback
override class✅ YES❌ NOsuper() + re-raise

钩子类型可抛出错误?需要提交?核心模式
doc_events (validate)✅ 是❌ 否收集错误,一次性抛出
doc_events (on_update)⚠️ 谨慎使用❌ 否隔离非关键操作
scheduler_events❌ 无意义✅ 是Try/except + log_error
permission_query_conditions❌ 绝不能❌ 否出错时返回空字符串
has_permission❌ 绝不能❌ 否出错时返回None
extend_bootinfo❌ 绝不能❌ 否Try/except + 回退值
override class✅ 是❌ 否super() + 重新抛出

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-hooks
    - Hooks syntax
  • erpnext-impl-hooks
    - Implementation workflows
  • erpnext-errors-controllers
    - Controller error handling
  • erpnext-errors-serverscripts
    - Server Script error handling
  • erpnext-syntax-hooks
    - Hooks语法
  • erpnext-impl-hooks
    - 实现工作流
  • erpnext-errors-controllers
    - 控制器错误处理
  • erpnext-errors-serverscripts
    - 服务器脚本错误处理