Exceptions
Fullfinity provides custom exception types for different error scenarios. These are handled by the middleware and displayed appropriately to users in the frontend.
Exception Hierarchy
Section titled “Exception Hierarchy”FullfinityError (base)├── User-Facing Errors (Toast - NO traceback)│ ├── UserError - Business logic errors (400)│ ├── ValidationError - Field/form validation errors (400)│ ├── AccessError - Permission denied (403)│ └── MissingError - Record not found (404)│└── Internal Errors (Modal - WITH traceback) ├── InternalError - Generic internal error (500) ├── ConfigurationError - Misconfiguration (500) └── ORMError - ORM/database layer bugs (500)Exception Types Summary
Section titled “Exception Types Summary”| Exception | HTTP | Frontend Display | Use Case |
|---|---|---|---|
UserError | 400 | Toast | User-facing business logic errors |
ValidationError | 400 | Toast | Field/form validation errors |
AccessError | 403 | Toast | Permission denied |
MissingError | 404 | Toast | Record not found |
InternalError | 500 | Modal + Traceback | Unexpected internal errors |
ConfigurationError | 500 | Modal + Traceback | System misconfiguration |
ORMError | 500 | Modal + Traceback | ORM/database layer bugs |
User-Facing Errors (Toast)
Section titled “User-Facing Errors (Toast)”These errors are expected and have user-friendly messages. They display as toast notifications without technical details.
UserError
Section titled “UserError”Use UserError for business logic errors that users can understand and potentially fix.
from fullfinity.engine.base import *
class Order(Model): _verbose_name = "Order"
async def action_confirm(self): if self.state != "Draft": raise UserError("Only draft orders can be confirmed.")
if not self.lines: raise UserError("Order must have at least one line.")
self.state = "Confirmed" await self.save()ValidationError
Section titled “ValidationError”Use ValidationError for data validation failures (format, constraints, required fields).
from fullfinity.engine.base import *
class Order(Model): quantity = Integer() discount = Float()
@Model.validate("quantity", "discount") async def check_values(self): if self.quantity < 0: raise ValidationError("Quantity must be positive") if self.discount > 100: raise ValidationError("Discount cannot exceed 100%")AccessError
Section titled “AccessError”Use AccessError when a user tries to perform an action they don’t have permission for.
from fullfinity.engine.base import *
class Order(Model): async def action_approve(self): if not await self.can_user_approve(): raise AccessError("You don't have permission to approve this order.")
self.state = "Approved" await self.save()
async def can_user_approve(self): env = env_ctx.get() return "sales.manager" in env.user_data.get("group_names", [])MissingError
Section titled “MissingError”Use MissingError when trying to access a record that doesn’t exist or has been deleted.
from fullfinity.engine.base import *
class Order(Model): async def action_view_invoice(self): invoice = await get_model("FinancialDocument").filter( source_order=self.id ).first()
if not invoice: raise MissingError("No invoice found for this order.")
return await get_action(model="FinancialDocument", record_id=invoice.id)Actionable Errors
Section titled “Actionable Errors”Any user-facing error can carry an action — an action dict that the error toast
renders as a button, so the user can jump straight to the screen that fixes the problem
instead of hitting a dead-end message. Pass it as the keyword-only action argument.
The action is the same shape the rest of the app dispatches (e.g. a window_action
pointing at a configuration screen, or a url). When present, the toast shows a button
labelled action["label"] (falling back to “Fix it”) that navigates there.
from fullfinity.engine.base import *
class CreateInvoiceWizard(Model): async def _get_down_payment_account(self, order): account_id = await get_model("Configuration").get_config( "default_downpayment_account", order.company.id ) if account_id: return await get_model("Account").filter(id=account_id).first()
# Dead-end message → one-click fix: the toast shows an "Open Invoicing # Settings" button that navigates to the configuration screen. raise UserError( "No down-payment account configured. Set a Down Payment Account " "(a Current Liability) in Invoicing settings.", action={ "type": "window_action", "identifier": "configuration_invoicing_setup_action", "label": "Open Invoicing Settings", }, )Notes:
actionis available on everyFullfinityErrorsubclass and defaults toNone, so plainraise UserError("…")is unchanged.- It is serialized into the error response (
{"error", "details", "action"}) and is most useful onUserError/ValidationError(the “you need to configure / set up X” class of errors). - Use a real, resolvable action
identifier(or aurl); an unresolvable target simply does nothing when clicked. - The same mechanism powers actionable notifications of any colour (success / info /
warning), not just errors — see the
notifyaction result in Actions.
Internal Errors (Modal + Traceback)
Section titled “Internal Errors (Modal + Traceback)”These errors indicate bugs or misconfigurations. They display as modal dialogs with full traceback for debugging.
InternalError
Section titled “InternalError”Use InternalError for unexpected internal errors (bugs, unexpected states).
from fullfinity.engine.base import *
class PaymentProcessor(Model): async def process_payment(self): result = await external_api.charge(self.amount)
if result.status not in ["success", "failed", "pending"]: # Unexpected state - this is a bug raise InternalError(f"Unexpected payment status: {result.status}")ConfigurationError
Section titled “ConfigurationError”Use ConfigurationError when the system is misconfigured or missing required settings.
from fullfinity.engine.base import *
class EmailService(Model): async def send_email(self): smtp_host = await self.get_param("smtp_host")
if not smtp_host: raise ConfigurationError("Missing required configuration: smtp_host")ORMError
Section titled “ORMError”Use ORMError for ORM-level bugs (type mismatches, field errors). This is primarily used internally by the ORM.
from fullfinity.engine.base import *
# ORMError is typically raised by the ORM itself, not in business logic# Example: assigning wrong type to a ManyToOne field# self.partner = "invalid" # Would raise ORMErrorError Display Behavior
Section titled “Error Display Behavior”| Exception Type | Modal Title | Shows Traceback | Log Level |
|---|---|---|---|
UserError | ”Error” | No | WARNING |
ValidationError | ”Validation Error” | No | WARNING |
AccessError | ”Access Denied” | No | WARNING |
MissingError | ”Not Found” | No | WARNING |
InternalError | ”Internal Error” | Yes | ERROR |
ConfigurationError | ”Configuration Error” | Yes | ERROR |
ORMError | ”ORM Error” | Yes | ERROR |
Catching Exceptions
Section titled “Catching Exceptions”Use FullfinityError to catch all Fullfinity-specific exceptions:
from fullfinity.engine.base import FullfinityError
try: await some_operation()except FullfinityError as e: # Catches all Fullfinity exceptions logger.error(f"Fullfinity error: {e}")Best Practices
Section titled “Best Practices”Choose the Right Exception
Section titled “Choose the Right Exception”-
UserError - User did something wrong they can fix
- “Invoice must have at least one line”
- “Cannot delete a posted document”
-
ValidationError - Data format/constraint issue
- “Invalid email format”
- “Amount must be positive”
-
AccessError - Permission issue
- “You don’t have permission to approve this order”
- “Only administrators can access this feature”
-
MissingError - Record not found
- “The record has been deleted”
- “Invoice not found”
-
InternalError - Unexpected bug
- “Unexpected state in processing”
- “Unknown action type”
-
ConfigurationError - System setup issue
- “Missing required configuration: SMTP_HOST”
- “Invalid database connection string”
-
ORMError - ORM layer bug (typically internal)
- “Expected JournalEntry instance for field ‘entry’”
- “Field ‘amount’ not found on model ‘Invoice‘“
Write Clear Error Messages
Section titled “Write Clear Error Messages”# Good - specific and actionableraise UserError("Cannot post invoice without lines. Add at least one line item.")raise UserError("Credit limit exceeded. Current limit: $5,000. Order total: $7,500.")raise AccessError("Only Sales Managers can approve orders over $10,000.")
# Bad - vagueraise UserError("Invalid operation")raise UserError("Error")Importing Exceptions
Section titled “Importing Exceptions”All exceptions are available from fullfinity.engine.base:
from fullfinity.engine.base import *# Includes: FullfinityError, UserError, AccessError, MissingError,# ValidationError, InternalError, ConfigurationError, ORMError
# Or import specifically:from fullfinity.engine.exceptions import ( FullfinityError, UserError, AccessError, MissingError, ValidationError, InternalError, ConfigurationError, ORMError,)