- ERPNext Database - Error Handling
- This skill covers error handling patterns for database operations. For syntax, see
- erpnext-database
- .
- Version
- v14/v15/v16 compatible Database Exception Types ┌─────────────────────────────────────────────────────────────────────┐ │ FRAPPE DATABASE EXCEPTIONS │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ frappe.DoesNotExistError │ │ └─► Document not found (get_doc, get_value with strict) │ │ │ │ frappe.DuplicateEntryError │ │ └─► Unique constraint violation (insert, rename) │ │ │ │ frappe.LinkExistsError │ │ └─► Cannot delete - linked documents exist │ │ │ │ frappe.ValidationError │ │ └─► General validation failure │ │ │ │ frappe.TimestampMismatchError │ │ └─► Concurrent edit detected (modified since load) │ │ │ │ frappe.db.InternalError │ │ └─► Database-level error (deadlock, connection lost) │ │ │ │ frappe.QueryTimeoutError (v15+) │ │ └─► Query exceeded timeout limit │ │ │ └─────────────────────────────────────────────────────────────────────┘ Main Decision: Error Handling by Operation ┌─────────────────────────────────────────────────────────────────────────┐ │ WHAT DATABASE OPERATION? │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ ► frappe.get_doc() / frappe.get_cached_doc() │ │ └─► Can raise DoesNotExistError │ │ └─► Check with frappe.db.exists() first OR catch exception │ │ │ │ ► doc.insert() / frappe.new_doc().insert() │ │ └─► Can raise DuplicateEntryError (unique constraints) │ │ └─► Can raise ValidationError (mandatory fields, custom validation) │ │ │ │ ► doc.save() │ │ └─► Can raise ValidationError │ │ └─► Can raise TimestampMismatchError (concurrent edit) │ │ │ │ ► doc.delete() / frappe.delete_doc() │ │ └─► Can raise LinkExistsError (linked documents) │ │ └─► Use force=True to ignore links (careful!) │ │ │ │ ► frappe.db.sql() / frappe.qb │ │ └─► Can raise InternalError (syntax, deadlock, connection) │ │ └─► Always use parameterized queries │ │ │ │ ► frappe.db.set_value() / doc.db_set() │ │ └─► Silently fails if record doesn't exist │ │ └─► No validation triggered │ │ │ └─────────────────────────────────────────────────────────────────────────┘ Error Handling Patterns Pattern 1: Safe Document Fetch
Option A: Check first (preferred for expected missing docs)
if frappe . db . exists ( "Customer" , customer_name ) : customer = frappe . get_doc ( "Customer" , customer_name ) else : frappe . throw ( _ ( "Customer '{0}' not found" ) . format ( customer_name ) )
Option B: Try/except (preferred when doc usually exists)
try : customer = frappe . get_doc ( "Customer" , customer_name ) except frappe . DoesNotExistError : frappe . throw ( _ ( "Customer '{0}' not found" ) . format ( customer_name ) )
Option C: Get with default (for optional lookups)
customer
frappe . db . get_value ( "Customer" , customer_name , "*" , as_dict = True ) if not customer :
Handle missing - no error raised
customer
{ "customer_name" : "Unknown" , "credit_limit" : 0 } Pattern 2: Safe Document Insert def create_customer ( data ) : """Create customer with duplicate handling.""" try : doc = frappe . get_doc ( { "doctype" : "Customer" , "customer_name" : data . get ( "name" ) , "customer_type" : data . get ( "type" , "Company" ) } ) doc . insert ( ) return { "success" : True , "name" : doc . name } except frappe . DuplicateEntryError :
Already exists - return existing
existing
frappe . db . get_value ( "Customer" , { "customer_name" : data . get ( "name" ) } ) return { "success" : True , "name" : existing , "existing" : True } except frappe . ValidationError as e : return { "success" : False , "error" : str ( e ) } Pattern 3: Safe Document Delete def delete_customer ( customer_name ) : """Delete customer with link handling.""" if not frappe . db . exists ( "Customer" , customer_name ) : frappe . throw ( _ ( "Customer '{0}' not found" ) . format ( customer_name ) ) try : frappe . delete_doc ( "Customer" , customer_name ) return { "success" : True } except frappe . LinkExistsError as e :
Get linked documents for user info
linked
get_linked_documents
(
"Customer"
,
customer_name
)
frappe
.
throw
(
_
(
"Cannot delete customer. Linked documents exist:
{0}"
)
.
format
(
"
"
.
join
(
[
f"•
{
l
[
'doctype'
]
}
:
{
l
[
'name'
]
}
"
for
l
in
linked
[
:
10
]
]
)
)
)
Pattern 4: Concurrent Edit Handling
def
update_document
(
doctype
,
name
,
updates
)
:
"""Update with concurrent edit detection."""
try
:
doc
=
frappe
.
get_doc
(
doctype
,
name
)
doc
.
update
(
updates
)
doc
.
save
(
)
return
{
"success"
:
True
}
except
frappe
.
TimestampMismatchError
:
Document was modified by another user
frappe . throw ( _ ( "This document was modified by another user. Please refresh and try again." ) , title = _ ( "Concurrent Edit Detected" ) ) except frappe . DoesNotExistError : frappe . throw ( _ ( "Document not found" ) ) Pattern 5: Batch Operations with Error Isolation def bulk_update_items ( items_data ) : """Bulk update with per-item error handling.""" results = { "success" : [ ] , "failed" : [ ] } for item_data in items_data : item_code = item_data . get ( "item_code" ) try : if not frappe . db . exists ( "Item" , item_code ) : results [ "failed" ] . append ( { "item" : item_code , "error" : "Item not found" } ) continue doc = frappe . get_doc ( "Item" , item_code ) doc . update ( item_data ) doc . save ( ) results [ "success" ] . append ( item_code ) except frappe . ValidationError as e : results [ "failed" ] . append ( { "item" : item_code , "error" : str ( e ) } ) except Exception as e : frappe . log_error ( frappe . get_traceback ( ) , f"Bulk update error: { item_code } " ) results [ "failed" ] . append ( { "item" : item_code , "error" : "Unexpected error" } ) return results Pattern 6: Safe SQL Query def get_sales_report ( customer , from_date , to_date ) : """Safe SQL query with error handling.""" try :
ALWAYS use parameterized queries
result
frappe
.
db
.
sql
(
"""
SELECT
customer,
SUM(grand_total) as total,
COUNT(*) as count
FROM tabSales Invoice
WHERE customer = %(customer)s
AND posting_date BETWEEN %(from_date)s AND %(to_date)s
AND docstatus = 1
GROUP BY customer
"""
,
{
"customer"
:
customer
,
"from_date"
:
from_date
,
"to_date"
:
to_date
}
,
as_dict
=
True
)
return
result
[
0
]
if
result
else
{
"total"
:
0
,
"count"
:
0
}
except
frappe
.
db
.
InternalError
as
e
:
frappe
.
log_error
(
frappe
.
get_traceback
(
)
,
"Sales Report Query Error"
)
frappe
.
throw
(
_
(
"Database error. Please try again or contact support."
)
)
See
:
references/patterns.md
for more error handling patterns.
Transaction Handling
Automatic Transaction Management
Frappe wraps each request in a transaction
On success: auto-commit
On exception: auto-rollback
def validate ( self ) :
All 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
Manual Savepoints (Advanced) def complex_operation ( ) : """Use savepoints for partial rollback."""
Create savepoint
frappe . db . savepoint ( "before_risky_op" ) try : risky_database_operation ( ) except Exception :
Rollback only to savepoint
frappe . db . rollback ( save_point = "before_risky_op" ) frappe . log_error ( frappe . get_traceback ( ) , "Risky Op Failed" )
Continue with alternative approach
safe_alternative_operation ( ) Scheduler/Background Jobs def background_task ( ) : """Background jobs need explicit commit.""" try : for record in records : process_record ( record )
REQUIRED in background jobs
frappe . db . commit ( ) except Exception : frappe . db . rollback ( ) frappe . log_error ( frappe . get_traceback ( ) , "Background Task Error" ) Critical Rules ✅ ALWAYS Check existence before get_doc - Or catch DoesNotExistError Use parameterized SQL queries - Never string formatting Handle DuplicateEntryError on insert - Unique constraints Commit in scheduler/background jobs - No auto-commit Log database errors with context - Include query/doc info Use db.exists() for existence checks - Not try/except get_doc ❌ NEVER Don't use string formatting in SQL - SQL injection risk Don't commit in controller hooks - Breaks transaction Don't ignore DoesNotExistError silently - Handle or log Don't assume db.set_value() succeeded - No error on missing doc Don't catch generic Exception for database ops - Catch specific types Quick Reference: Exception Handling
DoesNotExistError - Document not found
try : doc = frappe . get_doc ( "Customer" , name ) except frappe . DoesNotExistError : frappe . throw ( _ ( "Customer not found" ) )
DuplicateEntryError - Unique constraint violation
try : doc . insert ( ) except frappe . DuplicateEntryError :
Handle duplicate
LinkExistsError - Cannot delete linked document
try : frappe . delete_doc ( "Customer" , name ) except frappe . LinkExistsError : frappe . throw ( _ ( "Cannot delete - linked documents exist" ) )
TimestampMismatchError - Concurrent edit
try : doc . save ( ) except frappe . TimestampMismatchError : frappe . throw ( _ ( "Document was modified. Please refresh." ) )
InternalError - Database-level error
try : frappe . db . sql ( query ) except frappe . db . InternalError : frappe . log_error ( frappe . get_traceback ( ) , "Database Error" ) frappe . throw ( _ ( "Database error occurred" ) ) 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-database - Database operations syntax erpnext-errors-controllers - Controller error handling erpnext-errors-serverscripts - Server Script error handling erpnext-permissions - Permission patterns