← Back to list

erpnext-errors-database
by OpenAEC-Foundation
28 deterministic Claude AI skills for flawless ERPNext/Frappe v14-16 development. Agent Skills standard compliant.
⭐ 1🍴 1📅 Jan 23, 2026
SKILL.md
name: erpnext-errors-database description: "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."
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.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
Score
Total Score
75/100
Based on repository quality metrics
✓SKILL.md
SKILL.mdファイルが含まれている
+20
✓LICENSE
ライセンスが設定されている
+10
✓説明文
100文字以上の説明がある
+10
○人気
GitHub Stars 100以上
0/15
✓最近の活動
1ヶ月以内に更新
+10
○フォーク
10回以上フォークされている
0/5
✓Issue管理
オープンIssueが50未満
+5
✓言語
プログラミング言語が設定されている
+5
✓タグ
1つ以上のタグが設定されている
+5
Reviews
💬
Reviews coming soon

