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
frappe.throw()
✅ YES Dialog Error Log Validation errors
frappe.msgprint()
❌ NO Dialog No Warnings
frappe.log_error()
❌ NO No Error Log Debug/audit
frappe.publish_realtime()
❌ 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
frappe.ValidationError
417 Validation failures
frappe.PermissionError
403 Access denied
frappe.DoesNotExistError
404 Record not found
frappe.AuthenticationError
401 Not logged in
frappe.OutgoingEmailError
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: references/patterns.md for more error handling patterns.
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 frappe.db.escape() for user input in SQL - Prevent SQL injection
-
Add limit to queries in Scheduler scripts - Prevent memory issues
-
Call frappe.db.commit() in Scheduler scripts - Changes aren't auto-saved
-
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 raise statement - Use frappe.throw() instead
-
Don't call doc.save() in Before Save event - Framework handles it
-
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
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-serverscripts
-
Server Script syntax
-
erpnext-impl-serverscripts
-
Implementation workflows
-
erpnext-errors-clientscripts
-
Client-side error handling
-
erpnext-database
-
Database operations