Skip to docs content
Browse documentation
On this page

Fail loudly at the right layer

Fail where the code can still name the mistake and refuse the bad write, not later behind a quiet fallback.

Fail at the layer that can name the mistake and can still refuse the bad write. A quiet fallback further on hides which fact was missing, and the damage shows up somewhere else, later.

Example

Removing a package's SQL storage deletes both the database row and the Durable Object that holds the data. If the storage worker isn't reachable, Minion refuses the whole delete rather than dropping the row and orphaning the data:

if (row) {
    if (!transport) throw new SqlStorageError('Cannot remove package SQL storage while minion-storage is not configured.', 'unavailable')
    await call((t) => t.drop({ organizationId, packageId }))
    await db.delete(packageSqlDatabases).where(rowWhere(organizationId, packageId))
}

Job runs follow the same rule. When a run fails for good, Minion records the error on the run and the job before the queue gives up on it, so a failure never disappears without a trace. Find those errors with minion.runs.list({ status: 'error' }). See Troubleshooting .

When quiet is fine

A fallback is fine when nothing downstream depends on the answer. The "Needs attention" count in the app sidebar shows 0 if it can't be read. That's a badge, not a decision.

In your packages

Throw when an input or an API response is missing something you need. Don't return {} and let the next job work with empty data. A thrown error shows up in the run log with a stack; a quiet default shows up as a wrong email three days later.