- 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 Method Stops Execution? Rolls Back? User Sees? Use For frappe.throw() ✅ YES Depends on hook Dialog Validation errors raise Exception ✅ YES Depends on hook Error page Internal errors frappe.msgprint() ❌ NO ❌ NO Dialog Warnings frappe.log_error() ❌ NO ❌ NO Error Log Debug/audit Transaction Rollback by Hook Hook 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 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
(
"
"
.
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:
{0}"
)
.
format
(
"
"
.
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 Collect multiple validation errors - Better UX than one at a time Use try/except around external calls - APIs, file I/O, network Log unexpected errors - frappe.log_error(frappe.get_traceback()) Call super() in overridden methods - Preserve parent behavior Validate in before_submit - Last clean abort point for submittables Use _() for error messages - Enable translation ❌ NEVER Don't call frappe.db.commit() - Framework handles transactions Don't swallow errors silently - Always log unexpected exceptions Don't assume on_update can rollback doc - It's already saved Don't put critical logic in on_submit - Validate in before_submit 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 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-controllers - Controller syntax erpnext-impl-controllers - Implementation workflows erpnext-errors-serverscripts - Server Script error handling (sandbox) erpnext-errors-hooks - Hook error handling