Leantime/leantime · error · Exception

Task does not exist

Error message

Task does not exist

What it means

Tickets::canDelete($id) is the pre-delete guard used by the DelTicket controller (app/Domain/Tickets/Controllers/DelTicket.php:34) and, like every public service method, is callable over JSON-RPC as leantime.rpc.tickets.tickets.canDelete. It loads the ticket with getTicket($id); an empty result means no task with that id exists (already deleted, never existed, or wrong instance) and it throws Exception('Task does not exist').

Source

Thrown at app/Domain/Tickets/Services/Tickets.php:3711

        // Collaborator relationship rows are cleaned up inside the repository's delticket().
        if ($this->ticketRepository->delticket($id)) {

            TicketDeleted::dispatch(ticketId: (int) $id, legacyHook: __FUNCTION__);

            return true;
        }

        return false;
    }

    public function canDelete($id)
    {

        $ticket = $this->getTicket($id);

        if (empty($ticket)) {
            throw new \Exception('Task does not exist');
        }

        $hasLoggedHours = $this->timesheetsRepo->getTimesheetsByTicket($id);

        if ($hasLoggedHours) {
            throw new \Exception('Task has timesheets attached, delete all timesheets first or consider archiving the task');
        }

        return true;

    }

    /**
     * @return bool|string[]
     *
     * @throws BindingResolutionException
     *
     * @api

View on GitHub (pinned to 9a9f49f100)

Solutions

  1. Verify the task exists first: call getTicket($id) (or GET /tickets/showTicket/{id}) before attempting the delete.
  2. Refresh the board/task list in the UI before deleting to clear stale references.
  3. Make API/scripts idempotent: treat 'Task does not exist' as success (the end state you wanted is already true).
  4. Confirm you are talking to the right instance - check the id against zp_tickets.id in the DB you think you are using.

Example fix

// before
try {
    $this->ticketService->canDelete($id);
    $this->ticketService->delete($id);
} catch (\Exception $e) {
    // 'Task does not exist' indistinguishable from real failures
}

// after: check existence explicitly, treat a gone task as a no-op
if (empty($this->ticketService->getTicket($id))) {
    return response()->json(['status' => 'ok', 'detail' => 'already gone'], 200);
}
$this->ticketService->canDelete($id);
$this->ticketService->delete($id);
Defensive patterns

Strategy: validation

Validate before calling

if (empty($ticketService->getTicket($id))) {
    // treat as already-deleted: idempotent no-op instead of an exception
    return response()->json(['status' => 'ok', 'detail' => 'task already gone'], 200);
}
$ticketService->canDelete($id);

Type guard

/** True when a ticket with this id exists in the current instance. */
function ticketExists(\Leantime\Domain\Tickets\Services\Tickets $tickets, mixed $id): bool
{
    return ! empty($tickets->getTicket($id));
}

Try / catch

try {
    $ticketService->canDelete($id);
} catch (\Exception $e) {
    if ($e->getMessage() === 'Task does not exist') {
        return response()->json(['status' => 'ok', 'detail' => 'already deleted'], 200); // idempotent delete
    }
    throw $e; // timesheet guard and anything else stays an error
}

Prevention

When it happens

Trigger: GET/POST /tickets/delTicket/{id} with an id that is already gone or invalid; JSON-RPC canDelete called with a stale id; double-submit where the first request deleted the task and the second hits the guard; scripts using hard-coded ids after a re-import wiped and re-created tasks.

Common situations: Two browser tabs (or a stale kanban board) both offering delete on the same task; frontend caches referencing removed tasks; API automation pointing at the wrong environment/DB; id=0 or string ids that never resolve.

Related errors


AI-assisted analysis of Leantime/leantime@9a9f49f100 (2026-08-21). Data as JSON: /api/errors/6e44aa29521303a0. Report an issue: GitHub.