Get the FREE Ultimate OpenClaw Setup Guide →

erpnext-errors-controllers

Use Caution
npx machina-cli add skill OpenAEC-Foundation/ERPNext_Anthropic_Claude_Development_Skill_Package/erpnext-errors-controllers --openclaw
Files (1)
SKILL.md
16.0 KB

ERPNext Controllers - Error Handling

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


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                           │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

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                            │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

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

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

Error Handling Patterns

Pattern 1: Validation with Error Collection

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"))

Pattern 2: External API Call with Fallback

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."))

Pattern 3: Safe Post-Save Operations

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"
        )

Pattern 4: Submittable Document Error Handling

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."))

Pattern 5: Cancel with Cleanup

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"
        )

Pattern 6: Database Operation Error Handling

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.


Transaction Management

Understanding Transactions

# Frappe wraps each request in a transaction
# - 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!

Manual Savepoints (Advanced)

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"))

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

❌ 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

Quick Reference: Exception Handling

# 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)))

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-controllers - Controller syntax
  • erpnext-impl-controllers - Implementation workflows
  • erpnext-errors-serverscripts - Server Script error handling (sandbox)
  • erpnext-errors-hooks - Hook error handling

Source

git clone https://github.com/OpenAEC-Foundation/ERPNext_Anthropic_Claude_Development_Skill_Package/blob/main/skills/source/errors/erpnext-errors-controllers/SKILL.mdView on GitHub

Overview

This skill covers error handling patterns for ERPNext Document Controllers, including try/except, ValidationError, and PermissionError, with transaction management and rollback patterns. It explains how to log errors, show user-friendly messages, and maintain data integrity across v14–v16.

How This Skill Works

Implement error handling by wrapping critical controller logic in try/except blocks, catching specific errors such as ValidationError and PermissionError, and using frappe.throw for user-visible messages. Use frappe.log_error for silent logging and finally for cleanup. Understand rollback behavior varies by hook: validate rolls back the entire save, on_update may leave the doc saved, and on_submit can cause partial rollback, so design accordingly.

When to Use It

  • When performing data validation in validate or before_save and you need to rollback on error with a clear user message
  • When an operation in on_update or after_insert fails but the document is already saved and you need to log or handle non-critically
  • When a non-critical permission issue arises and you want to log it without breaking flow
  • When submitting a document in before_submit or on_submit and you must prevent submission or clearly indicate failure
  • When performing cleanup or rollback steps across hooks (e.g., on_cancel or in finally blocks) to ensure data integrity

Quick Start

  1. Step 1: Identify critical controller logic and wrap it in a try/except block, targeting specific errors (ValidationError, PermissionError)
  2. Step 2: Use frappe.throw for user-facing errors and add frappe.log_error for non-critical issues requiring audit
  3. Step 3: Add finally blocks or explicit rollback hooks appropriate to the current lifecycle hook (validate, on_update, before_submit, on_submit)

Best Practices

  • Wrap sensitive controller logic in try/except and catch specific error types (ValidationError, PermissionError)
  • Use frappe.throw for user-visible errors and rely on proper rollback semantics per hook
  • Log non-critical errors with frappe.log_error to aid debugging without failing the user flow
  • Keep error handling close to the operation that may fail and provide clear, actionable messages
  • Test across hooks (validate, on_update, before_submit, on_submit) to understand rollback implications and user feedback

Example Use Cases

  • A controller validates required fields in validate; upon ValidationError, the save is rolled back and a user-friendly message is shown
  • An on_update flow catches PermissionError, logs the issue with frappe.log_error, and continues without interrupting the user unnecessarily
  • During before_submit, a frappe.throw prevents submission when business rules fail, ensuring the doc remains a draft until fixed
  • An on_submit path uses try/except to handle errors gracefully and logs details for audit while ensuring the submitted state is consistent
  • In on_cancel, rollback steps are performed with careful exception handling to reverse operations and clean up related records

Frequently Asked Questions

Add this skill to your agents
Sponsor this space

Reach thousands of developers