gatsbyjs/gatsby · error · Error
The plugin "${_.get(action, `plugin.name`, `anonymous`)}" tr
Error message
The plugin "${_.get(action, `plugin.name`, `anonymous`)}" tried to end a job with the id "${action.payload.id}" that either hasn't yet been created or has already been ended What it means
The v1 jobs reducer's END_JOB case looks up action.payload.id in state.jobs.active via findIndex; if no active job has that id it throws, because ending a job that was never created (or was already ended) indicates a lifecycle bug. The message names the offending plugin (defaulting to 'anonymous') and the offending id. This guards against double-end and end-without-create.
Source
Thrown at packages/gatsby/src/redux/reducers/jobs.ts:43
state.active[index] = mergedJob
return state
} else {
state.active.push({
...action.payload,
createdAt: Date.now(),
plugin: action.plugin,
})
return state
}
}
case `END_JOB`: {
if (!action.payload.id) {
throw new Error(`An ID must be provided when ending a job`)
}
const completedAt = Date.now()
const index = _.findIndex(state.active, j => j.id === action.payload.id)
if (index === -1) {
throw new Error(oneLine`
The plugin "${_.get(action, `plugin.name`, `anonymous`)}"
tried to end a job with the id "${action.payload.id}"
that either hasn't yet been created or has already been ended`)
}
const job = state.active.splice(index, 1)[0]
state.done.push({
...job,
completedAt,
runTime: moment(completedAt).diff(moment(job.createdAt)),
})
return state
}
}
return state
}
View on GitHub (pinned to 8b06340921)
Solutions
- Ensure every endJob is preceded by a successful createJob/setJob with the same id.
- Guard against double-end by tracking local 'ended' state per job id in your plugin.
- Use stable, content-digest-based ids so re-runs do not end already-completed jobs.
- Switch to the v2 job API which de-duplicates by content digest.
Example fix
// before
actions.endJob({ id: jobId })
// may run again on rebuild ->
// after
const ended = new Set()
function finish(id) {
if (ended.has(id)) return
ended.add(id)
actions.endJob({ id })
} Defensive patterns
Strategy: try-catch
Validate before calling
// Track active job ids locally to avoid double-end / end-without-create.
const active = new Set()
function start(actions, id) { active.add(id); actions.createJob({ id }) }
function finish(actions, id) {
if (!active.has(id)) return // never created or already ended
active.delete(id)
actions.endJob({ id })
} Type guard
function isActiveJob(state, id) {
return state.jobs.active.some(j => j.id === id)
} Try / catch
try {
actions.endJob({ id })
} catch (e) {
if (/tried to end a job/.test(e.message)) {
// already ended or never started - safe to ignore, mark locally
} else throw e
} Prevention
- Maintain a local Set of pending job ids and delete on end.
- Use content-digest ids so repeated work collapses to the same job.
- Guard endJob in lifecycle hooks that may run more than once.
When it happens
Trigger: Dispatching END_JOB for an id that is not in state.jobs.active: either the job was never created via CREATE_JOB/SET_JOB, or it was already ended by a prior END_JOB (active is mutated via splice so a second end misses).
Common situations: Calling endJob twice for the same job; ending a job whose create failed or was skipped; plugin lifecycle mismatch where onPostBootstrap ends a job started in a different (skipped) hook; cached/replayed job state after a partial build.
Related errors
- An ID must be provided when creating or setting job
- An ID must be provided when ending a job
- If you encounter this error, it's probably a Gatsby internal
- You must pass an object into setPluginStatus. What was passe
- generateImageSource must be a function
AI-assisted analysis of gatsbyjs/gatsby@8b06340921 (2026-08-13).
Data as JSON: /api/errors/3123c23ecbe1f60b.
Report an issue: GitHub.