skip to content

In Django's Tasks framework, how do you check a task's outcome later from another request using TaskResult and get_result()?

level: middleimportance: should knowfreq 24%

answer

  1. keep the result id
  2. four status values
  3. a snapshot until refreshed
  4. return_value raises until success

basics

~10 s

Store TaskResult.id, then call task.get_result(id) or the backend's get_result(id) and read status: READY, RUNNING, FAILED or SUCCESSFUL. return_value works only after success; errors holds failures. Built-in backends cannot fetch results across processes.

solid answer

~40 s

`enqueue()` returns a `TaskResult`; I save its `id`, for example on the `Photo` row, and a later request calls `make_thumbnail.get_result(result_id)` or `default_task_backend.get_result(result_id)` (`aget_result()` in async code). The result is a snapshot: `status` is one of `READY`, `RUNNING`, `FAILED`, `SUCCESSFUL`, and `refresh()` reloads it. `return_value` raises `ValueError` unless the status is `SUCCESSFUL`, which distinguishes a task that returned `None` from one that has not finished. On failure, `errors` holds `TaskError` entries with the exception class path and a traceback string. `get_result()` raises `TaskResultDoesNotExist` for an unknown id and `TaskResultMismatch` when the id belongs to another task; on a backend without result support it raises `NotImplementedError`, which includes `ImmediateBackend`.

code

python · 11 lines
python
from django.tasks import TaskResultStatus

from .tasks import make_thumbnail

result = make_thumbnail.get_result(result_id)
if not result.is_finished:
    result.refresh()
if result.status == TaskResultStatus.SUCCESSFUL:
    thumb_pk = result.return_value
elif result.status == TaskResultStatus.FAILED:
    last_error = result.errors[-1].traceback

go deeper

for a junior

Recall that enqueue() returns a TaskResult with an id and a status of READY, RUNNING, FAILED or SUCCESSFUL.

for a middle

Explain get_result() on the task versus the backend, refresh() for snapshots, and why return_value raises until success.

for a senior

Build a status endpoint around a backend that supports results, handle TaskResultDoesNotExist, and log failure details the TaskError does not keep.

for a principal

Decide whether task results or domain fields on your own models are the source of truth for user-visible progress, given backend retention differs.

## The TaskResult Every `enqueue()` returns a **`TaskResult`**, an immutable record of one execution of a task. Its main attributes: - `id` — a unique string used to look the result up later. - `status` — a `TaskResultStatus` value. - `enqueued_at`, `started_at`, `finished_at`, `last_attempted_at` — timestamps, `None` until reached. - `args`, `kwargs` — the JSON-normalised arguments. - `errors` — a list of `TaskError` objects. - `worker_ids` — the workers that processed it; `attempts` is its length. - `is_finished` — `True` for `FAILED` or `SUCCESSFUL`. - `return_value` — the task's return value, guarded (see below). ## The four statuses | Status | Meaning | |---|---| | `READY` | Just enqueued, or ready to be run again | | `RUNNING` | A worker is executing it | | `FAILED` | It raised, or could not start | | `SUCCESSFUL` | It finished without raising | ## Retrieving it from another request A thumbnail upload returns immediately; a later request (say, a polling endpoint) wants to know whether the thumbnail is ready: ```python from django.http import JsonResponse from django.shortcuts import get_object_or_404 from django.tasks import TaskResultStatus from django.tasks.exceptions import TaskResultDoesNotExist from .models import Photo from .tasks import make_thumbnail def thumbnail_status(request, pk): photo = get_object_or_404(Photo, pk=pk) try: result = make_thumbnail.get_result(photo.thumbnail_task_id) except TaskResultDoesNotExist: return JsonResponse({"status": "unknown"}, status=404) if result.status == TaskResultStatus.FAILED: return JsonResponse({"status": "failed"}) return JsonResponse({"status": result.status}) ``` Two ways to look a result up: 1. **`Task.get_result(id)`** — also checks that the result belongs to *this* task and raises `TaskResultMismatch` otherwise. 2. **`backend.get_result(id)`** (for example `default_task_backend.get_result(id)`) — any task's result. Both raise `TaskResultDoesNotExist` for an unknown id, and both have `aget_result()` variants for async code. ## Snapshots and refresh() A `TaskResult` reflects the state **at the moment it was fetched**; it does not update itself. `result.refresh()` (or `await result.arefresh()`) reloads status, timestamps, errors, worker ids and the return value from the backend. ## return_value and errors `return_value` is deliberately strict: - `SUCCESSFUL` → the JSON-normalised return value (possibly `None`). - `FAILED` → raises `ValueError("Task failed")`. - anything else → raises `ValueError("Task has not finished yet")`. That design separates "the task returned `None`" from "there is no value yet". For failures, `errors` contains `TaskError` entries with `exception_class_path`, a lazily resolved `exception_class`, and a `traceback` string. The exception instance itself is not kept; its message survives only as text inside the traceback, so log structured details inside the task. ## Backend support Result retrieval is a backend capability (`supports_get_result`): | Backend | `get_result()` from another request | |---|---| | `ImmediateBackend` (default) | Not supported; raises `NotImplementedError` | | `DummyBackend` | Only results held in memory in the same process; statuses stay `READY` | | A production backend with a result store | Usually supported | So the polling endpoint above only makes sense with a production backend. With `ImmediateBackend`, the `TaskResult` returned by `enqueue()` is already final, and you would record the outcome on the model directly. ## Practical advice - Store the result `id` on the domain row if users need to see progress. - Poll with `refresh()` rather than re-enqueueing. - Do not treat the result store as a permanent audit log; retention is up to the backend.

  • Why does TaskResult.return_value raise instead of returning None before the task finishes?
    Because `None` is a legitimate return value. Raising `ValueError` for `READY`, `RUNNING` and `FAILED` means a caller can never mistake 'not done' or 'failed' for 'succeeded and returned nothing'. Check `status` or `is_finished` first.
  • What information about a failure survives in a TaskResult?
    Each `TaskError` keeps the exception's import path, a lazily resolved `exception_class`, and the formatted traceback string, which includes the message as text. The exception object and its attributes are not stored, so structured details the caller needs should be logged or written to the database by the task itself.

saying these in an interview costs you the question

  • Expects a TaskResult to update its status on its own over time
  • Reads return_value without checking the status first
  • Polls results with ImmediateBackend and gets NotImplementedError
  • Assumes errors keeps the original exception object so it can be re-raised
  • Uses Task.get_result() with an id from a different task