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