erpnext-errors-database
Scannednpx machina-cli add skill OpenAEC-Foundation/ERPNext_Anthropic_Claude_Development_Skill_Package/erpnext-errors-database --openclawERPNext 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.mdfor 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 syntaxerpnext-errors-controllers- Controller error handlingerpnext-errors-serverscripts- Server Script error handlingerpnext-permissions- Permission patterns
Source
git clone https://github.com/OpenAEC-Foundation/ERPNext_Anthropic_Claude_Development_Skill_Package/blob/main/skills/source/errors/erpnext-errors-database/SKILL.mdView on GitHub Overview
This skill teaches robust ERPNext/Frappe database error handling for DoesNotExistError, DuplicateEntryError, transaction failures, and query errors. It emphasizes retry patterns and maintaining data integrity across V14, V15, and V16. Practical guidance helps build resilient ERPNext apps that gracefully handle common DB pitfalls.
How This Skill Works
The approach maps each database operation to its potential exceptions and recommended handling. For get_doc/get_cached_doc, catching DoesNotExistError or pre-checking with exists() prevents crashes; inserts catch DuplicateEntryError and ValidationError; saves may raise TimestampMismatchError for concurrent edits; deletes may raise LinkExistsError when linked docs exist. Retry strategies and transaction boundaries are used to preserve integrity during transient errors and long-running queries.
When to Use It
- When reading documents with get_doc/get_cached_doc and handling missing records
- When inserting new documents that must respect unique constraints
- When saving edits that could be overwritten by concurrent updates
- When deleting documents that may be linked to other docs
- When executing long-running queries that risk timeouts or DB errors
Quick Start
- Step 1: Identify the ERPNext DB operation (get_doc, insert, save, delete) and wrap with try/except for the relevant exceptions
- Step 2: Add existence checks (frappe.db.exists()) or catch DoesNotExistError before proceeding
- Step 3: Implement retry logic for transient errors and define clear transaction boundaries to protect data integrity
Best Practices
- Check existence before accessing data to avoid DoesNotExistError
- Catch and translate DB exceptions into clear, actionable messages
- Implement controlled retries for transient errors with backoff
- Use transactions / savepoints to maintain data integrity
- Respect link constraints on delete; use force=True only when appropriate
Example Use Cases
- Fetching a customer document that might not exist and returning a friendly not-found response
- Inserting a product with a unique SKU and handling DuplicateEntryError
- Updating an order where another process may concurrently modify the same record (TimestampMismatchError)
- Deleting an invoice that has linked payment records, requiring LinkExistsError handling
- Running a sales report with a large query that could hit a QueryTimeoutError