| name | frappe-errors-hooks |
| description | Use when debugging hooks.py errors in Frappe/ERPNext. Covers hook not firing (typo, wrong dict structure), circular imports, app_include_js path errors, scheduler_events not running, doc_events on wrong DocType, permission_query_conditions SQL errors, override_doctype_class import failures, extend_doctype_class [v16+] conflicts, fixtures not loading. Error diagnosis by hook type for v14/v15/v16. Keywords: hooks.py error, hook not firing, scheduler not running,, hook not working, scheduler not running, app_include not loading, override not applied. doc_events error, circular import, fixtures error, override class error.
|
| license | MIT |
| compatibility | Claude Code, Claude.ai Projects, Claude API. Frappe v14-v16. |
| metadata | {"author":"OpenAEC-Foundation","version":"2.0"} |
Frappe Hooks Error Diagnosis & Resolution
Cross-ref: frappe-syntax-hooks (syntax), frappe-impl-hooks (workflows), frappe-errors-controllers (controller errors).
Error-to-Fix Mapping Table
| Error / Symptom | Cause | Fix |
|---|
| Hook not firing at all | Typo in dotted path | Verify module path matches actual file location |
ImportError on bench start | Wrong module path or circular import | Fix import path; break circular dependency |
AttributeError: module has no attribute | Function name typo in hooks.py | Match function name exactly to Python definition |
app_include_js not loading | Path missing assets/ prefix or wrong extension | Use "assets/myapp/js/file.js" format |
| scheduler_events not running | Scheduler disabled or workers down | bench scheduler enable, check bench doctor |
| doc_events handler never called | DocType name misspelled in dict key | Use exact DocType name with spaces: "Sales Invoice" |
permission_query_conditions breaks list view | SQL syntax error or frappe.throw() in handler | Return valid SQL string; NEVER throw |
override_doctype_class import failure | Parent class import path changed between versions | Pin import to correct module path for target version |
extend_doctype_class [v16+] method conflict | Two extensions define same method name | Rename conflicting methods; check hook resolution order |
| Fixtures not loading on install | Wrong dt key or DocType doesn't exist on target | Verify DocType exists before export; check filter syntax |
extend_bootinfo breaks login | Unhandled exception in boot handler | Wrap ALL bootinfo code in try/except |
Wildcard "*" handler breaks all saves | Unhandled exception in wildcard doc_events | ALWAYS wrap wildcard handlers in try/except |
| Hook fires but changes lost | Missing frappe.db.commit() in scheduler | Add explicit commit in scheduler/background tasks |
| Multiple handler chain broken | First handler throws, others never run | Isolate non-critical ops in try/except |
Hook Registration Errors
Hook Not Firing: Diagnosis Checklist
IS YOUR HOOK NOT FIRING?
│
├─► Check 1: Is the dotted path correct?
│ hooks.py: "myapp.events.sales.validate"
│ File: myapp/events/sales.py → def validate(doc, method=None):
│ COMMON MISTAKE: "myapp.events.sales_invoice.validate" when file is sales.py
│
├─► Check 2: Is the dict structure correct?
│ doc_events uses NESTED dict: {"Sales Invoice": {"validate": "path"}}
│ scheduler_events uses LIST: {"daily": ["path1", "path2"]}
│ permission_query uses FLAT dict: {"Sales Invoice": "path"}
│
├─► Check 3: Is bench restarted after hooks.py change?
│ ALWAYS run: bench restart (or bench clear-cache for dev)
│
├─► Check 4: Is the DocType name exact?
│ "Sales Invoice" NOT "SalesInvoice" NOT "sales_invoice"
│ Use exact DocType name as shown in Frappe UI
│
└─► Check 5: Is the app installed on the site?
bench --site mysite list-apps
Circular Import Errors
Rule: NEVER import from hooks.py in your event handlers. hooks.py is read by the framework, not imported by your code.
Wrong Dict Structure by Hook Type
doc_events = {
"Sales Invoice": "myapp.events.validate"
}
doc_events = {
"Sales Invoice": {
"validate": "myapp.events.sales.validate"
}
}
scheduler_events = {
"daily": "myapp.tasks.daily_sync"
}
scheduler_events = {
"daily": ["myapp.tasks.daily_sync"]
}
scheduler_events = {
"cron": ["0 9 * * *", "myapp.tasks.morning"]
}
scheduler_events = {
"cron": {
"0 9 * * 1-5": ["myapp.tasks.morning_report"]
}
}
app_include_js / app_include_css Errors
app_include_js = "js/myapp.js"
app_include_js = "myapp.public.js.myapp"
app_include_js = "assets/myapp/js/myapp.js"
app_include_js = ["assets/myapp/js/app.js", "assets/myapp/js/utils.js"]
app_include_css = "assets/myapp/css/myapp.css"
Diagnosis: If JS/CSS not loading, check browser DevTools Network tab for 404. Run bench build after adding new files. ALWAYS verify the file exists at myapp/public/js/myapp.js.
scheduler_events Not Running
Diagnosis Steps
bench scheduler status
bench doctor
bench execute frappe.utils.scheduler.get_all_tasks
Common Scheduler Failures
def daily_sync():
for item in frappe.get_all("Item", limit=100):
frappe.db.set_value("Item", item.name, "synced", 1)
def daily_sync():
for item in frappe.get_all("Item", limit=100):
frappe.db.set_value("Item", item.name, "synced", 1)
frappe.db.commit()
def daily_task():
try:
process_records()
except Exception:
pass
def daily_task():
try:
process_records()
frappe.db.commit()
except Exception:
frappe.log_error(frappe.get_traceback(), "Daily Task Error")
doc_events Errors
Error Handling by Event Phase
| Event | Throw Effect | Transaction | Pattern |
|---|
validate | Prevents save, full rollback | Pre-write | Collect errors, throw once |
before_save | Prevents save, full rollback | Pre-write | Same as validate |
on_update | Doc already saved, error shown | Post-write | Isolate non-critical ops |
after_insert | Doc already saved, error shown | Post-write | Isolate non-critical ops |
on_submit | Doc already submitted | Post-write | Isolate non-critical ops |
on_cancel | Doc already cancelled | Post-write | Isolate non-critical ops |
Multiple Handler Chain Problem
def validate(doc, method=None):
"""Collect errors, throw once at end."""
errors = []
if doc.grand_total < 0:
errors.append(_("Total cannot be negative"))
if errors:
frappe.throw("<br>".join(errors))
def on_update(doc, method=None):
try:
send_notification(doc)
except Exception:
frappe.log_error(frappe.get_traceback(), f"Notify error: {doc.name}")
try:
sync_external(doc)
except Exception:
frappe.log_error(frappe.get_traceback(), f"Sync error: {doc.name}")
NEVER Commit in doc_events
def on_update(doc, method=None):
frappe.db.set_value("Counter", "main", "count", 100)
frappe.db.commit()
def on_update(doc, method=None):
frappe.db.set_value("Counter", "main", "count", 100)
Permission Hook Errors
permission_query_conditions: NEVER Throw
def query_conditions(user):
if "Sales User" not in frappe.get_roles(user):
frappe.throw("Access denied")
return f"owner = '{user}'"
def query_conditions(user):
try:
user = user or frappe.session.user
if "System Manager" in frappe.get_roles(user):
return ""
return f"`tabSales Invoice`.owner = {frappe.db.escape(user)}"
except Exception:
frappe.log_error(frappe.get_traceback(), "Query Conditions Error")
return f"`tabSales Invoice`.owner = {frappe.db.escape(frappe.session.user)}"
Note: permission_query_conditions only affects frappe.db.get_list(), NOT frappe.db.get_all().
has_permission: NEVER Throw
def has_permission(doc, user=None, permission_type=None):
if doc.status == "Locked":
frappe.throw("Locked")
def has_permission(doc, user=None, permission_type=None):
try:
user = user or frappe.session.user
if doc.status == "Locked" and permission_type == "write":
return False
return None
except Exception:
frappe.log_error(frappe.get_traceback(), "Permission Error")
return None
Override & Extend Errors
override_doctype_class: Import Failures
override_doctype_class = {
"Sales Invoice": "myapp.overrides.CustomSI"
}
from erpnext.accounts.doctype.sales_invoice.sales_invoice import SalesInvoice
class CustomSalesInvoice(SalesInvoice):
def validate(self):
try:
super().validate()
except frappe.ValidationError:
raise
except Exception:
frappe.log_error(frappe.get_traceback(), "Parent validate error")
raise
self.custom_validation()
Warning: Only ONE app's override_doctype_class is active per DocType ("last writer wins"). Use extend_doctype_class [v16+] for multi-app compatibility.
extend_doctype_class [v16+]: Conflicts
extend_doctype_class = {
"Sales Invoice": ["myapp.extensions.si.SalesInvoiceMixin"]
}
class SalesInvoiceMixin:
def myapp_custom_calc(self):
"""Prefixed to avoid conflicts with other extensions."""
pass
extend_bootinfo Errors
def extend_boot(bootinfo):
settings = frappe.get_single("My Settings")
bootinfo.config = settings.config
def extend_boot(bootinfo):
bootinfo.myapp_config = {}
try:
if frappe.db.exists("My Settings", "My Settings"):
settings = frappe.get_single("My Settings")
bootinfo.myapp_config = {"feature": settings.feature or False}
except Exception:
frappe.log_error(frappe.get_traceback(), "Bootinfo Error")
Fixtures Not Loading
fixtures = [{"doctype": "Custom Field", "filters": [...]}]
fixtures = [{"dt": "Custom Field", "filters": [["module", "=", "My App"]]}]
fixtures = [{"dt": "My Custom DocType"}]
Export command: bench --site mysite export-fixtures
Import: Automatic during bench --site mysite install-app myapp
Critical Rules
ALWAYS
- Restart bench after changing hooks.py
- Use try/except in scheduler tasks — no user sees errors
- Call
frappe.db.commit() in scheduler — no auto-commit
- Return safe fallbacks in permission hooks — NEVER throw
- Call
super() in override classes — re-raise ValidationError
- Wrap
extend_bootinfo in try/except — errors break login
- Wrap wildcard
"*" doc_events in try/except — errors break ALL saves
- Prefix extend_doctype_class [v16+] methods with app name
NEVER
- Throw in
permission_query_conditions — breaks list views
- Throw in
has_permission — breaks document access
- Commit in doc_events — breaks transaction management
- Import from hooks.py in event handlers — causes circular imports
- Assume single handler — multiple apps register doc_events
- Use string formatting in permission SQL — SQL injection risk
- Ignore scheduler errors — they fail completely silently
Quick Reference: Error Handling by Hook Type
| Hook Type | Can Throw? | Commit? | Error Strategy |
|---|
| doc_events (validate) | YES | NEVER | Collect errors, throw once |
| doc_events (on_update+) | Careful | NEVER | Isolate non-critical ops |
| scheduler_events | Pointless | ALWAYS | try/except + log_error |
| permission_query_conditions | NEVER | NEVER | Return "" or owner filter |
| has_permission | NEVER | NEVER | Return None on error |
| extend_bootinfo | NEVER | NEVER | try/except + safe defaults |
| override_doctype_class | YES | NEVER | super() + re-raise |
| extend_doctype_class [v16+] | YES | NEVER | Prefix methods, avoid conflicts |
| fixtures | N/A | N/A | Verify dt key and DocType existence |
| app_include_js/css | N/A | N/A | Check assets/ prefix, run bench build |
Reference Files
| File | Contents |
|---|
references/patterns.md | Complete error handling patterns by hook type |
references/examples.md | Full working examples with error handling |
references/anti-patterns.md | Common mistakes with wrong/correct pairs |
See Also
frappe-syntax-hooks — Hook syntax and dict structures
frappe-impl-hooks — Implementation workflows
frappe-errors-controllers — Controller error handling
frappe-errors-database — Database error handling
frappe-errors-serverscripts — Server Script error handling