Skip to content

Exceptions

Fullfinity provides custom exception types for different error scenarios. These are handled by the middleware and displayed appropriately to users in the frontend.

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)
ExceptionHTTPFrontend DisplayUse Case
UserError400ToastUser-facing business logic errors
ValidationError400ToastField/form validation errors
AccessError403ToastPermission denied
MissingError404ToastRecord not found
InternalError500Modal + TracebackUnexpected internal errors
ConfigurationError500Modal + TracebackSystem misconfiguration
ORMError500Modal + TracebackORM/database layer bugs

These errors are expected and have user-friendly messages. They display as toast notifications without technical details.

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()

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%")

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", [])

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)

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:

  • action is available on every FullfinityError subclass and defaults to None, so plain raise UserError("…") is unchanged.
  • It is serialized into the error response ({"error", "details", "action"}) and is most useful on UserError / ValidationError (the “you need to configure / set up X” class of errors).
  • Use a real, resolvable action identifier (or a url); 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 notify action result in Actions.

These errors indicate bugs or misconfigurations. They display as modal dialogs with full traceback for debugging.

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}")

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")

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 ORMError
Exception TypeModal TitleShows TracebackLog Level
UserError”Error”NoWARNING
ValidationError”Validation Error”NoWARNING
AccessError”Access Denied”NoWARNING
MissingError”Not Found”NoWARNING
InternalError”Internal Error”YesERROR
ConfigurationError”Configuration Error”YesERROR
ORMError”ORM Error”YesERROR

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}")
  1. UserError - User did something wrong they can fix

    • “Invoice must have at least one line”
    • “Cannot delete a posted document”
  2. ValidationError - Data format/constraint issue

    • “Invalid email format”
    • “Amount must be positive”
  3. AccessError - Permission issue

    • “You don’t have permission to approve this order”
    • “Only administrators can access this feature”
  4. MissingError - Record not found

    • “The record has been deleted”
    • “Invoice not found”
  5. InternalError - Unexpected bug

    • “Unexpected state in processing”
    • “Unknown action type”
  6. ConfigurationError - System setup issue

    • “Missing required configuration: SMTP_HOST”
    • “Invalid database connection string”
  7. ORMError - ORM layer bug (typically internal)

    • “Expected JournalEntry instance for field ‘entry’”
    • “Field ‘amount’ not found on model ‘Invoice‘“
# Good - specific and actionable
raise 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 - vague
raise UserError("Invalid operation")
raise UserError("Error")

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,
)