Error Handling

Every failure PormG raises for a domain problem is a subtype of PormGError, so one catch clause covers the whole surface:

try
    M.Result.objects.filter("driverid__surname" => "Senna").update("points" => 25)
catch e
    e isa PormGError || rethrow()
    @error "PormG rejected the write" msg=error_message(e) type=typeof(e)
end

rethrow() on anything that is not a PormGError matters: your own exceptions, and Julia-level misuse such as a missing keyword argument, still propagate as themselves. Only driver failures are wrapped into the taxonomy.

This page is the task-oriented view — which operation raises what, and how to react. For the full type-by-type reference, including every umbrella and the fields each type carries, see Error taxonomy in the API reference.

Which operation raises what

Catch the umbrella when you want a category, the concrete type when you want a reaction.

Reading

CallRaisesWhen
get(...)DoesNotExistNo row matched
get(...)MultipleObjectsReturnedMore than one row matched; carries count
earliest(...) / latest(...)DoesNotExistEmpty queryset — unlike first()/last(), which return nothing
row.driverid on an unprojected ForeignKey / OneToOneFieldLazyTraversalErrorPormG never lazily loads a relation — project it with values(...)
any filter or valuesUnknownFieldErrorThe field name does not exist on the model (lookups are case-sensitive)
any filterFilterErrorThe predicate itself is malformed
PostgreSQL-only features on SQLiteBackendCapabilityErrore.g. iunaccent_*, JSONB containment, window frame=, with_advisory_lock(...; on_missing_lock = :error)
anything else about query shapeQueryBuildErrorThe long-tail default

Writing

CallRaisesWhen
create / update / delete, bulk, M2M mutatorsWritesDisabledErrorThe connection has change_data: false — see Creating Records
update / delete with no filterUnsafeMutationErrorRefused as unsafe; delete accepts allow_delete_all = true
delete with limit/offset/order_by/distinct/aggregatesUnsafeMutationErrorThose shapes make cascade counting unreliable — see Deleting Records
delete of a row referenced with on_delete = PROTECTProtectedErrorThe data forbids it; reassign or delete the referencing rows first
create with a null value on a non-null fieldInvalidValueErrorRejected by PormG before any statement is sent
create / bulk_insert violating a constraintIntegrityErrorThe database refused it — UNIQUE, FOREIGN KEY, NOT NULL, CHECK

Transactions and connections

CallRaisesWhen
atomic(durable = true) inside an open transactionTransactionErrorIt must be the outermost transaction
a model bound to another connection, inside a transactionTransactionErrorOpen the transaction on that model's own connection
any query, connection lost mid-flightOperationalErrorTransient. Retry the whole transaction, never the statement
any query, pool saturatedPoolTimeoutErrorRaise pool_size/pool_timeout — see Advanced Configuration
any query, database unreachablePoolConnectErrorCarries the cause and a redacted connection string
with_advisory_lock(...; wait = false)OperationalErrorLock held elsewhere. Never raised on SQLite — see Advisory Locks
with_advisory_lock(...; on_missing_lock = :error) on SQLiteBackendCapabilityErrorSQLite has no advisory locks; the body would run unprotected, so it is refused instead

Configuration and migrations

CallRaisesWhen
Configuration.load(...)PormG.Configuration.MissingConfigurationErrorNo connection.yml found; try PormG.setup(path)
Configuration.load(...)InvalidConfigurationErrorUnknown or missing adapter, unsupported extension, bad extensions shape, an environment block that is not a block of settings
model definitionFieldValidationError / ModelDefinitionErrorBad field argument / bad model shape. Catch DefinitionError for both
makemigrations / migrateInvalidMigrationErrorThe migration or the schema it describes is not valid
migrate on a destructive planPormG.Migrations.DestructiveMigrationErrorNon-interactive run without destructive = true; carries statements
Two types need a qualified name

DestructiveMigrationError and MissingConfigurationError are not on the using PormG surface — reach them as PormG.Migrations.DestructiveMigrationError and PormG.Configuration.MissingConfigurationError. Catching their umbrellas (MigrationError, ConfigurationError) works unqualified.

Reading a caught error

Use error_message(e), not e.msg. Most types carry a msg field, but eight do not — they carry structured fields instead, and e.msg on those is a FieldError:

TypeFields instead of msg
DoesNotExistmodel_name, filters
MultipleObjectsReturnedmodel_name, count, filters
PoolTimeoutErroradapter, pool_size, max_size, attempts, elapsed_seconds
PoolConnectErroradapter, cause, connection, attempts, elapsed_seconds
IntegrityError, OperationalError, StatementErroradapter, cause
DestructiveMigrationErrormsg, statements

error_message renders any of them to a plain String, so it is always safe:

catch e
    e isa PormGError || rethrow()
    @error "failed" msg=error_message(e)
end

Catching a whole category

The abstract umbrellas exist so a handler can name a family without listing its members:

try
    PormG.Configuration.load("db_2")
    PormG.Migrations.migrate()
catch e
    if e isa ConfigurationError
        @error "Fix connection.yml and retry" msg=error_message(e)
    elseif e isa MigrationError
        @error "The migration plan was rejected" msg=error_message(e)
    else
        rethrow()
    end
end

The umbrellas are FieldAccessError, DefinitionError, ConfigurationError, MigrationError, PoolError and DatabaseError, all under PormGError.

DatabaseError is the boundary worth understanding: it means the statement reached the database and was refused there. A value PormG rejects before sending — a null on a non-null field, an unknown column — never gets that far and raises the query-side type instead.

Coming from an older PormG

The taxonomy replaced the untyped errors and driver-native exceptions PormG used to raise, across several releases — so a catch block written against an older version may no longer match. The deliberate clean break is spelled out in Error taxonomy.

If you are upgrading an app, UPGRADING.md carries the greps and the concrete before → after edits for each step — run PormG.upgrade_guide(from = v"<your pinned version>") to see only what applies to you, and read Upgrading PormG for the workflow.