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:<br>{0}").format(
"<br>".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