erpnext-errors-hooks

安装

npx skills add https://github.com/openaec-foundation/erpnext_anthropic_claude_development_skill_package --skill erpnext-errors-hooks
ERPNext Hooks - Error Handling
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 Error Handling Overview ┌─────────────────────────────────────────────────────────────────────┐ │ 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 │ │ │ └─────────────────────────────────────────────────────────────────────┘ 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 Error Handling Transaction Behavior (Same as Controllers) Event frappe.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 Multiple Handler Chain

hooks.py - Multiple apps can register handlers

App A

doc_events

{ "Sales Invoice" : { "validate" : "app_a.events.validate_si"

Runs first

} }

App B

doc_events

{ "Sales Invoice" : { "validate" : "app_b.events.validate_si"

Runs second

} }

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

Pattern: Validate Handler

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 ( "
" . join ( errors ) ) Pattern: on_update Handler (Isolated Operations) 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 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 │ │ │ └─────────────────────────────────────────────────────────────────────┘ Pattern: Scheduler Task with Error Handling

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" ) Pattern: Scheduler with Batch Commits 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!

❌ WRONG - Breaks list view entirely!

def query_conditions ( user ) : if not user : frappe . throw ( "User required" )

DON'T DO THIS!

return f"owner = ' { user } '"

✅ 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 ) } " has_permission - NEVER Throw!

❌ 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!

✅ 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 Override Hooks Error Handling override_doctype_class

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" ) ) extend_doctype_class (V16+)

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" ) ) extend_bootinfo Error Handling Critical: Errors Break Page Load!

❌ 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

✅ 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 = { } Critical Rules ✅ ALWAYS Use try/except in scheduler tasks - No user feedback otherwise Call frappe.db.commit() in scheduler - Changes aren't auto-saved Return safe fallbacks in permission hooks - Never throw Call super() in override classes - Preserve parent behavior Log errors with context - Include document name, operation Wrap extend_bootinfo in try/except - Errors break page load ❌ NEVER Don't throw in permission_query_conditions - Breaks list views Don't throw in has_permission - Breaks document access Don't assume single handler - Multiple apps can register Don't commit in doc_events - Framework handles transactions Don't ignore scheduler errors - They fail silently Quick Reference: Error Handling by Hook Hook Type Can Throw? Commit? Key Pattern doc_events (validate) ✅ YES ❌ NO Collect errors, throw once doc_events (on_update) ⚠️ Careful ❌ NO Isolate non-critical ops scheduler_events ❌ Pointless ✅ YES Try/except + log_error permission_query_conditions ❌ NEVER ❌ NO Return "" on error has_permission ❌ NEVER ❌ NO Return None on error extend_bootinfo ❌ NEVER ❌ NO Try/except + fallback override class ✅ YES ❌ NO super() + re-raise Reference Files File Contents references/patterns.md Complete error handling patterns references/examples.md Full working examples references/anti-patterns.md Common mistakes to avoid 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

返回排行榜