erpnext-errors-hooks

ERPNext Hooks - Error Handling

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "erpnext-errors-hooks" with this command: npx skills add openaec-foundation/erpnext_anthropic_claude_development_skill_package/openaec-foundation-erpnext-anthropic-claude-development-skill-package-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("<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

  • 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

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

erpnext-code-interpreter

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

erpnext-syntax-jinja

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

erpnext-impl-controllers

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

erpnext-syntax-customapp

No summary provided by upstream source.

Repository SourceNeeds Review