Claude-skill-registry erpnext-errors-serverscripts
Error handling patterns for ERPNext Server Scripts. Use when handling sandbox errors, frappe.throw usage, validation in server scripts, and debugging. V14/V15/V16 compatible. Triggers: server script error, frappe.throw, sandbox error, validation error, debugging server script.
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/erpnext-errors-serverscripts" ~/.claude/skills/majiayu000-claude-skill-registry-erpnext-errors-serverscripts && rm -rf "$T"
manifest:
skills/data/erpnext-errors-serverscripts/SKILL.mdsource content
ERPNext Server Scripts - Error Handling
This skill covers error handling patterns for Server Scripts. For syntax, see
erpnext-syntax-serverscripts. For implementation workflows, see erpnext-impl-serverscripts.
Version: v14/v15/v16 compatible
CRITICAL: Sandbox Limitations for Error Handling
┌─────────────────────────────────────────────────────────────────────┐ │ ⚠️ SANDBOX RESTRICTIONS AFFECT ERROR HANDLING │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ ❌ NO try/except blocks (blocked in RestrictedPython) │ │ ❌ NO raise statements (use frappe.throw instead) │ │ ❌ NO import traceback │ │ │ │ ✅ frappe.throw() - Stop execution, show error │ │ ✅ frappe.log_error() - Log to Error Log doctype │ │ ✅ frappe.msgprint() - Show message, continue execution │ │ ✅ Conditional checks before operations │ │ │ └─────────────────────────────────────────────────────────────────────┘
Main Decision: How to Handle the Error?
┌─────────────────────────────────────────────────────────────────────────┐ │ WHAT TYPE OF ERROR ARE YOU HANDLING? │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ ► Validation error (must stop save/submit)? │ │ └─► frappe.throw() with clear message │ │ │ │ ► Warning (inform user, allow continue)? │ │ └─► frappe.msgprint() with indicator │ │ │ │ ► Log error for debugging (no user impact)? │ │ └─► frappe.log_error() │ │ │ │ ► API error response (HTTP error)? │ │ └─► frappe.throw() with exc parameter OR set response │ │ │ │ ► Scheduler task error? │ │ └─► frappe.log_error() + continue processing other items │ │ │ │ ► Prevent operation but not with error dialog? │ │ └─► Return early + frappe.msgprint() │ │ │ └─────────────────────────────────────────────────────────────────────────┘
Error Methods Reference
Quick Reference
| Method | Stops Execution? | User Sees? | Logged? | Use For |
|---|---|---|---|---|
| ✅ YES | Dialog | Error Log | Validation errors |
| ❌ NO | Dialog | No | Warnings |
| ❌ NO | No | Error Log | Debug/audit |
| ❌ NO | Toast | No | Background updates |
frappe.throw() - Stop Execution
# Basic throw - stops execution, rolls back transaction frappe.throw("Customer is required") # With title frappe.throw("Amount cannot be negative", title="Validation Error") # With exception type (for API scripts) frappe.throw("Not authorized", exc=frappe.PermissionError) frappe.throw("Record not found", exc=frappe.DoesNotExistError) # With formatted message frappe.throw( f"Credit limit exceeded. Limit: {credit_limit}, Requested: {amount}", title="Credit Check Failed" )
Exception Types for API Scripts:
| Exception | HTTP Code | Use For |
|---|---|---|
| 417 | Validation failures |
| 403 | Access denied |
| 404 | Record not found |
| 401 | Not logged in |
| 500 | Email send failed |
frappe.log_error() - Silent Logging
# Basic error log frappe.log_error("Something went wrong", "My Script Error") # With context data frappe.log_error( f"Failed to process invoice {doc.name}: {error_detail}", "Invoice Processing Error" ) # Log current exception (in controllers, not sandbox) frappe.log_error(frappe.get_traceback(), "Unexpected Error")
frappe.msgprint() - Warning Without Stopping
# Simple warning frappe.msgprint("Stock is running low", indicator="orange") # With title frappe.msgprint( "This customer has pending payments", title="Warning", indicator="yellow" ) # Alert style (top of page) frappe.msgprint( "Document will be processed in background", alert=True )
Error Handling Patterns by Script Type
Pattern 1: Document Event - Validation
# Type: Document Event # Event: Before Save # Collect all errors, show together errors = [] if not doc.customer: errors.append("Customer is required") if doc.grand_total <= 0: errors.append("Total must be greater than zero") if not doc.items: errors.append("At least one item is required") else: for idx, item in enumerate(doc.items, 1): if not item.item_code: errors.append(f"Row {idx}: Item Code is required") if (item.qty or 0) <= 0: errors.append(f"Row {idx}: Quantity must be positive") # Throw all errors at once if errors: frappe.throw("<br>".join(errors), title="Validation Errors")
Pattern 2: Document Event - Conditional Warning
# Type: Document Event # Event: Before Save # Warning: doesn't stop save credit_limit = frappe.db.get_value("Customer", doc.customer, "credit_limit") or 0 if credit_limit > 0 and doc.grand_total > credit_limit: frappe.msgprint( f"Order total ({doc.grand_total}) exceeds credit limit ({credit_limit})", title="Credit Warning", indicator="orange" )
Pattern 3: Document Event - Safe Database Lookup
# Type: Document Event # Event: Before Save # Always validate before database lookup if doc.customer: customer_data = frappe.db.get_value( "Customer", doc.customer, ["credit_limit", "disabled", "territory"], as_dict=True ) # Check if customer exists if not customer_data: frappe.throw(f"Customer {doc.customer} not found") # Check if disabled if customer_data.disabled: frappe.throw(f"Customer {doc.customer} is disabled") # Use the data doc.territory = customer_data.territory
Pattern 4: API Script - Error Responses
# Type: API # Method: get_customer_info customer = frappe.form_dict.get("customer") # Validate required parameter if not customer: frappe.throw("Parameter 'customer' is required", exc=frappe.ValidationError) # Check existence if not frappe.db.exists("Customer", customer): frappe.throw(f"Customer '{customer}' not found", exc=frappe.DoesNotExistError) # Check permission if not frappe.has_permission("Customer", "read", customer): frappe.throw("You don't have permission to view this customer", exc=frappe.PermissionError) # Success response frappe.response["message"] = { "customer": customer, "credit_limit": frappe.db.get_value("Customer", customer, "credit_limit") }
Pattern 5: Scheduler - Batch Processing with Error Isolation
# Type: Scheduler Event # Cron: 0 9 * * * (daily at 9:00) processed = 0 errors = [] invoices = frappe.get_all( "Sales Invoice", filters={"status": "Unpaid", "docstatus": 1}, fields=["name", "customer"], limit=100 # ALWAYS limit in scheduler ) for inv in invoices: # Isolate errors per item - don't let one failure stop all if not frappe.db.exists("Customer", inv.customer): errors.append(f"{inv.name}: Customer not found") continue # Safe processing result = process_invoice(inv.name) if result.get("success"): processed += 1 else: errors.append(f"{inv.name}: {result.get('error', 'Unknown error')}") # Log summary if errors: frappe.log_error( f"Processed: {processed}, Errors: {len(errors)}\n\n" + "\n".join(errors), "Invoice Processing Summary" ) # REQUIRED: commit in scheduler frappe.db.commit() def process_invoice(invoice_name): """Helper function with error handling""" # Validate invoice exists if not frappe.db.exists("Sales Invoice", invoice_name): return {"success": False, "error": "Invoice not found"} # Process logic here return {"success": True}
Pattern 6: Permission Query - Safe Fallback
# Type: Permission Query # DocType: Sales Invoice # Safe role check user_roles = frappe.get_roles(user) or [] if "System Manager" in user_roles: conditions = "" # Full access elif "Sales Manager" in user_roles: # Manager sees team's invoices team = frappe.db.get_value("User", user, "department") if team: conditions = f"`tabSales Invoice`.department = {frappe.db.escape(team)}" else: conditions = f"`tabSales Invoice`.owner = {frappe.db.escape(user)}" elif "Sales User" in user_roles: # User sees only own invoices conditions = f"`tabSales Invoice`.owner = {frappe.db.escape(user)}" else: # No access - return impossible condition conditions = "1=0"
See:
for more error handling patterns.references/patterns.md
Transaction Behavior
Automatic Rollback on frappe.throw()
# Type: Document Event - Before Save # All changes roll back if throw is called doc.status = "Processing" # This change... frappe.db.set_value("Counter", "main", "count", 100) # ...and this... if some_condition_fails: frappe.throw("Validation failed") # ...are ALL rolled back
Manual Commit in Scheduler
# Type: Scheduler Event # Changes are NOT auto-committed in scheduler for item in items: frappe.db.set_value("Item", item.name, "last_sync", frappe.utils.now()) # REQUIRED: Explicit commit frappe.db.commit()
Partial Commit Pattern (Scheduler)
# Type: Scheduler Event # Process in batches with intermediate commits BATCH_SIZE = 50 items = frappe.get_all("Item", filters={"sync_pending": 1}, limit=500) for i in range(0, len(items), BATCH_SIZE): batch = items[i:i + BATCH_SIZE] for item in batch: frappe.db.set_value("Item", item.name, "sync_pending", 0) # Commit after each batch - partial progress saved frappe.db.commit()
Critical Rules
✅ ALWAYS
- Validate inputs before database operations - Check existence before get_doc
- Use
for user input in SQL - Prevent SQL injectionfrappe.db.escape() - Add
to queries in Scheduler scripts - Prevent memory issueslimit - Call
in Scheduler scripts - Changes aren't auto-savedfrappe.db.commit() - Collect multiple errors before throwing - Better user experience
- Log errors in Scheduler scripts - No user to see the error
❌ NEVER
- Don't use try/except in Server Scripts - Blocked by sandbox
- Don't use
statement - Useraise
insteadfrappe.throw() - Don't call
in Before Save event - Framework handles itdoc.save() - Don't assume database values exist - Always check first
- Don't ignore empty results - Handle gracefully
Quick Reference: Error Message Quality
# ❌ BAD - Technical, not actionable frappe.throw("KeyError: customer") frappe.throw("NoneType has no attribute 'name'") frappe.throw("Query failed") # ✅ GOOD - Clear, actionable frappe.throw("Please select a customer before saving") frappe.throw(f"Customer '{doc.customer}' not found. Please verify the customer exists.") frappe.throw("Could not calculate totals. Please ensure all items have valid quantities.")
Reference Files
| File | Contents |
|---|---|
| Complete error handling patterns |
| Full working examples |
| Common mistakes to avoid |
See Also
- Server Script syntaxerpnext-syntax-serverscripts
- Implementation workflowserpnext-impl-serverscripts
- Client-side error handlingerpnext-errors-clientscripts
- Database operationserpnext-database