Skip to main content

Troubleshooting

Two channels tell you what went wrong, and you'll usually want both:

  • Typed errors — thrown synchronously at the call site (init, addJob, …). Catch PortQueueError and branch on error.code.
  • Events — emitted asynchronously as jobs move through the system on every instance. This is how you see failures, retries, stalls, and — crucially — the swallowed internal errors that never reach a try/catch.

Catch typed errors​

Catch PortQueueError (the base) and branch on error.code (PortQueueErrorCode) — never on message strings.

import { PortQueueError, PortQueueErrorCode } from '@portqueue/core';

try {
await pq.addJob({ queue: 'emails', data });
} catch (err) {
if (err instanceof PortQueueError && err.code === PortQueueErrorCode.QUEUE_PAUSED) {
// handle the specific case
}
}

Error codes​

CodeError classWhat it means & the fix
NOT_STARTEDNotStartedErrorYou called a method before await pq.init(). The message names the method. Await init() first.
INVALID_OPTIONSInvalidOptionsErrorAn options object failed validation (e.g. both schedule.cron and schedule.startEverySeconds, a one-off delay combined with a recurring mode, or a duration that isn't a non-negative number of seconds). Read the message; it names the offending field.
UNSUPPORTED_CAPABILITYUnsupportedCapabilityErrorThe driver's engine can't do what you asked. Use a driver that supports it or the documented fallback.
SCHEMA_VERSION_MISMATCHSchemaVersionMismatchErrorThe DB schema version differs from the library's. Run portqueue migrate (see Going to Production).
MIGRATION_FAILEDMigrationFailedErrorA migration step errored. Check DB permissions and the underlying error; re-run migrate.
DRIVER_CONNECTION_ERRORDriverConnectionErrorCouldn't connect. Check connection string, network, credentials, and that the DB client peer dep is installed.
DRIVER_QUERY_ERRORDriverQueryErrorA query failed at the engine. Inspect the wrapped cause; often a permissions or schema issue.
QUEUE_NOT_FOUNDQueueNotFoundErrorAn admin client (the dashboard or CLI) was asked to produce onto a queue with no row. Have the application declare it (or add a job to it) first — an admin tool never creates queues. Also raised by pauseQueue/resumeQueue for a name nothing has ever used.
QUEUE_PAUSEDQueuePausedErrorAn add was refused because the queue is paused with PauseMode.ALL. Resume it, or expect this during a drain.
QUEUE_IN_USEQueueInUseErrorremoveQueue refused because the queue is still declared in this process (by createQueue). Remove it from an admin client, or stop declaring it here first.
INSTANCE_ACTIVEInstanceActiveErrorremoveInstance refused because the instance was last seen less than 30 minutes ago. Wait until it has been unseen that long — a live instance would just re-register.
SHUTDOWN_TIMEOUTShutdownTimeoutErrorshutdown() gave up on in-flight work after timeoutSeconds. The client is stopped and the driver disconnected either way; raise the timeout or make long handlers honour job.signal.
MISSING_PEER_DEPENDENCYMissingPeerDependencyErrorThe driver's client (e.g. pg, better-sqlite3) isn't installed. Install the peer shown in the message.
Every code has a class

Each code above is carried by the class beside it, and every one of them is instanceof PortQueueError. There are no codes without a class: a code nothing can throw would be a promise the library doesn't keep, so the never-thrown placeholders that used to sit here (ALREADY_STARTED, ALREADY_STOPPED, IDEMPOTENCY_CONFLICT, SINGLETON_CONFLICT, MIGRATION_CHECKSUM_MISMATCH) are gone. init() is idempotent, dedup returns result.deduplicated rather than raising, and a migration is identified by its version alone.

There is no JOB_NOT_FOUND for the same reason: every by-id operation already says "not found" in its return type — getJob gives null, removeJob/cancelJob give false, retryJob gives null — which is also what lets a bulk action skip a stale id instead of failing the whole batch.

Exported error classes​

Every class below is importable from @portqueue/core and extends PortQueueError. Each instance carries .code (a PortQueueErrorCode string enum value) and .context (an object with the offending fields). Branching on err.code is still the most robust check.

Class.codeMeaningTypical fix
PortQueueError(any)Base class for every error the library throws.Branch on err.code.
NotStartedErrorNOT_STARTEDA method was called before await pq.init().Await init() first.
QueueNotFoundErrorQUEUE_NOT_FOUNDA headless (dashboard/CLI) client named a queue with no row, or a pause targeted a name nothing has used.Declare the queue in your application first.
InvalidOptionsErrorINVALID_OPTIONSAn options object failed validation.Read the message; it names the offending field.
UnsupportedCapabilityErrorUNSUPPORTED_CAPABILITYThe driver's engine can't do what was asked.Use a capable driver or the documented fallback.
SchemaVersionMismatchErrorSCHEMA_VERSION_MISMATCHDB schema version differs from the library's.Run portqueue migrate.
MigrationFailedErrorMIGRATION_FAILEDA migration step errored.Check DB permissions and the cause; re-run migrate.
DriverConnectionErrorDRIVER_CONNECTION_ERRORCouldn't connect to the DB.Check connection string, network, credentials, peer dep.
DriverQueryErrorDRIVER_QUERY_ERRORA query failed at the engine.Inspect the wrapped cause (permissions/schema).
QueuePausedErrorQUEUE_PAUSEDadd refused because the queue is paused with PauseMode.ALL.Resume the queue, or expect this during a drain.
QueueInUseErrorQUEUE_IN_USEremoveQueue refused because the queue is still declared in this process.Stop declaring it here, or remove it from an admin client.
InstanceActiveErrorINSTANCE_ACTIVEremoveInstance refused because the instance was seen less than 30 minutes ago.Wait until it has been unseen for 30 minutes.
ShutdownTimeoutErrorSHUTDOWN_TIMEOUTGraceful shutdown didn't finish within timeoutSeconds.Raise the timeout or make handlers honor job.signal.
MissingPeerDependencyErrorMISSING_PEER_DEPENDENCYThe driver's client peer dep isn't installed.Install the peer shown in the message.

Diagnose with events​

A try/catch only covers failures on your call path. Everything that happens inside the claim loops — a handler throwing, a job stalling because an instance died, an internal error the tool swallows to stay up — surfaces as an event, never an exception. Subscribe on the client to watch it live and wire it to your logs/metrics:

import { PortQueueEvent } from '@portqueue/core';

// The catch-all: any swallowed internal error surfaces here. Subscribe to this in production.
pq.on(PortQueueEvent.ERROR, ({ error, source }) => {
logger.error('portqueue internal error', { source, error });
});

// Follow a failing job through its retries.
pq.on(PortQueueEvent.JOB_FAILED, ({ queue, jobId, error, retryAttempt }) => {
logger.warn('job failed', { queue, jobId, retryAttempt, error });
});

When a symptom is fuzzy, the fastest diagnosis is to watch the right event:

SymptomWatchWhat it tells you
Something "just doesn't work" but nothing throwsERRORA swallowed internal error — the single most valuable subscription.
A handler keeps re-runningJOB_FAILED, JOB_RETRYINGThe handler is throwing; inspect error and retryAttempt.
Jobs go quiet, then reappearJOB_STALLEDA lock lapsed (instance died or the handler ran past lockDurationSeconds); another instance reclaims it.
An enqueue seems to do nothingJOB_ADDED (deduplicated: true)The job was deduped by idempotencyKey / singletonKey.
Throughput collapses under loadSYSTEM_PRESSURE_HIGH / _NORMALBackpressure engaged — see Rate limiting.
A schedule never firesSCHEDULE_FIRED (absent)The definition isn't active, or no instance is running the queue.

Listeners are non-blocking and receive one typed payload object — see the full events reference for every event and its fields. To block or change an operation rather than observe it, use a hook instead.

Common situations​

  • A handler keeps re-running — it's throwing. Watch JOB_FAILED / JOB_RETRYING and inspect the error. Failed jobs stay in the job table (there is no dead-letter queue), so you can inspect them and retryJob.
  • Nothing is processing — the queue may be paused (QUEUE_PAUSED), or no running process declared it with a handler. Confirm a consumer tier is up (watch WORKER_STARTED).
  • Jobs are never picked up — a job sits enqueued forever with no JOB_ACTIVE. Usual causes:
    • Affinity mismatch — the job's preferredInstanceKeyPattern matches no live instance, so nothing is eligible to run it. It waits indefinitely until a matching instanceKey comes online. Widen the pattern or start an instance whose key matches.
    • Queue paused — the queue is paused with PauseMode.PROCESSING, so add still succeeds but nothing dequeues. Resume it.
    • Nothing consumes the queue — no running instance declared it with a handler, so its jobs just accumulate. Add the handler to that queue's createQueue call (watch WORKER_STARTED).
  • Schema / table-prefix mismatch — a CLI invocation or a second app sees no queues/jobs (or raises SCHEMA_VERSION_MISMATCH) even though the primary app is busy. This happens when the --table-prefix / --schema / --namespace given to the CLI (or a second driver) differs from what the app's driver actually uses, so they read different tables. Make the prefix, schema, and namespace match the app's driver configuration exactly.
  • Jobs stall or run twice — an instance died mid-job. The lock lapses after lockDurationSeconds and another instance reclaims the job (JOB_STALLED). Make long handlers honor job.signal so they stop when their lock is lost.
  • Silent failures — always subscribe to PortQueueEvent.ERROR; swallowed internal errors surface only there.
  • Schema errors on boot — SCHEMA_VERSION_MISMATCH. Run portqueue migrate (see Going to Production).