Claude-skill-registry erpnext-errors-database
Error handling patterns for ERPNext/Frappe database operations. Use when handling DoesNotExistError, DuplicateEntryError, transaction failures, and query errors. Covers retry patterns and data integrity. V14/V15/V16 compatible. Triggers: database error, DoesNotExistError, DuplicateEntryError, transaction failed, query error.
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/erpnext-errors-database" ~/.claude/skills/majiayu000-claude-skill-registry-erpnext-errors-database && rm -rf "$T"
manifest:
skills/data/erpnext-errors-database/SKILL.mdsource content
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:
for more error handling patterns.references/patterns.md
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 |
|---|---|
| Complete error handling patterns |
| Full working examples |
| Common mistakes to avoid |
See Also
- Database operations syntaxerpnext-database
- Controller error handlingerpnext-errors-controllers
- Server Script error handlingerpnext-errors-serverscripts
- Permission patternserpnext-permissions