danny-avila/LibreChat · info · Error

Run cancelled

Error message

Run cancelled

What it means

Thrown by waitForRun when the ABORT_KEYS cache entry for the conversation is set to 'cancelled'. This is intentional control flow: a user (or system) marked the run for cancellation, so polling stops and the cancellation propagates up as an exception. It is a signal, not an infrastructure fault.

Source

Thrown at api/server/services/Runs/handle.js:135

    if (run.status !== lastSeenStatus) {
      logger.debug(`[${run.status}] ${runInfo}`);
      lastSeenStatus = run.status;
    }

    logger.debug(`[heartbeat ${i}] ${runStatus}`);

    let cancelStatus;
    try {
      const timeoutMessage = `[heartbeat ${i}] ${runIdLog} | Cancel Status check operation timed out.`;
      cancelStatus = await withTimeout(cache.get(cacheKey), raceTimeoutMs, timeoutMessage);
    } catch (error) {
      logger.warn(`Error retrieving cancel status: ${error}`);
    }

    if (cancelStatus === 'cancelled') {
      logger.warn(`[waitForRun] ${runStatus} | RUN CANCELLED`);
      throw new Error('Run cancelled');
    }

    if (![RunStatus.IN_PROGRESS, RunStatus.QUEUED].includes(run.status)) {
      logger.debug(`[FINAL] ${runInfo} | status: ${run.status}`);
      await runManager.fetchRunSteps({
        openai,
        thread_id: thread_id,
        run_id: run_id,
        runStatus: run.status,
        final: true,
      });
      break;
    }

    // may use in future; for now, just fetch from the final status
    await runManager.fetchRunSteps({
      openai,
      thread_id: thread_id,

View on GitHub (pinned to 5ff282f900)

Solutions

  1. Treat this exception as expected: catch it and report a 'cancelled' state to the client rather than logging it as an error.
  2. If the cancellation is spurious, clear the ABORT_KEYS entry for the conversation key before starting a new run.
  3. Ensure the cancel flag is written under the same key format `${userId}:${conversationId}` that waitForRun reads.

Example fix

// before
await waitForRun({ openai, run_id, thread_id, runManager });

// after
try {
  await waitForRun({ openai, run_id, thread_id, runManager });
} catch (err) {
  if (err.message === 'Run cancelled') {
    return { status: 'cancelled' };
  }
  throw err;
}
Defensive patterns

Strategy: try-catch

Validate before calling

const cancelStatus = await cache.get(`${userId}:${conversationId}`);
if (cancelStatus === 'cancelled') {
  return { status: 'cancelled' };
}

Type guard

const isCancelled = async (cache, key) => (await cache.get(key)) === 'cancelled';

Try / catch

try {
  await waitForRun({ openai, run_id, thread_id, runManager });
} catch (err) {
  if (err.message === 'Run cancelled') {
    return { status: 'cancelled' };
  }
  throw err;
}

Prevention

When it happens

Trigger: The user clicks stop/abort on an in-progress assistant run; a parent flow sets cache.set(`${userId}:${conversationId}`, 'cancelled') under ABORT_KEYS; a stale cancel flag from a previous run on the same conversation key is still present.

Common situations: UI stop button; automated test that aborts; an earlier run on the same conversation was cancelled and the cache key was never cleared.

Related errors


AI-assisted analysis of danny-avila/LibreChat@5ff282f900 (2026-08-12). Data as JSON: /api/errors/05345f11c15c15f3. Report an issue: GitHub.