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
*
* @apiView on GitHub (pinned to 9a9f49f100)
Solutions
- Verify the task exists first: call getTicket($id) (or GET /tickets/showTicket/{id}) before attempting the delete.
- Refresh the board/task list in the UI before deleting to clear stale references.
- Make API/scripts idempotent: treat 'Task does not exist' as success (the end state you wanted is already true).
- 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
- Check existence (getTicket) before every delete call.
- Make delete flows idempotent - 'already gone' is success for a DELETE.
- Refresh stale boards/lists before deleting from the UI.
- In API scripts, resolve ids dynamically instead of caching them across re-imports.
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.