askill
erpnext-errors-hooks

erpnext-errors-hooksSafety 88Repository

Error handling patterns for ERPNext hooks.py configurations. Use when debugging doc_events errors, scheduler failures, boot session issues, and app initialization problems. V14/V15/V16 compatible. Triggers: hooks.py error, doc_events error, scheduler error, boot session error, app initialization error.

12 stars
1.2k downloads
Updated 2/13/2026

Package Files

Loading files...
SKILL.md

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)

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

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("<br>".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

  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

❌ 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

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

Reference Files

FileContents
references/patterns.mdComplete error handling patterns
references/examples.mdFull working examples
references/anti-patterns.mdCommon 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

Install

Download ZIP
Requires askill CLI v1.0+

AI Quality Score

78/100Analyzed 2/18/2026

Comprehensive ERPNext hooks error handling skill with actionable code patterns, clear decision trees for different hook types (doc_events, scheduler_events, permissions, overrides), and strong safety guidance. Well-structured with ASCII diagrams and tables. Includes explicit when-to-use triggers. Located in dedicated skills folder with observability tag. Minor deduction for referencing other skills for syntax and being specific to ERPNext framework."

88
85
65
72
82

Metadata

Licenseunknown
Version-
Updated2/13/2026
PublisherOpenAEC-Foundation

Tags

observability