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

  1. Ensure every endJob is preceded by a successful createJob/setJob with the same id.
  2. Guard against double-end by tracking local 'ended' state per job id in your plugin.
  3. Use stable, content-digest-based ids so re-runs do not end already-completed jobs.
  4. 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

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


AI-assisted analysis of gatsbyjs/gatsby@8b06340921 (2026-08-13). Data as JSON: /api/errors/3123c23ecbe1f60b. Report an issue: GitHub.