Celery link_error errback example (tested)
Celery's link_error runs an errback task when a task fails. Define it as def on_error(request, exc, traceback), pass link_error=on_error.s(), and use chain(...).on_error(on_error.s()) for chains.
Setup used in every block
common.py sets task_always_eager so tasks and their callbacks run inline without a broker, and defines an errback with the current signature, (request, exc, traceback):
from celery import Celery
app = Celery('tasks')
app.conf.task_always_eager = True # run inline: no broker or worker needed here
@app.task
def add(x, y):
return x + y
@app.task
def div(x, y):
return x / y
@app.task
def on_error(request, exc, traceback): # errback signature: (request, exc, traceback)
print(f'errback: task {request.task} id={request.id[:8]} failed with {exc!r}')
1. link_error on a single task
from common import *
r = div.apply_async((1, 0), link_error=on_error.s())
print('state:', r.state, '| result:', repr(r.result))
Output:
errback: task common.div id=a749705b failed with ZeroDivisionError('division by zero')
state: FAILURE | result: ZeroDivisionError('division by zero')
Pass the errback as a signature (on_error.s()) in link_error=. When the task raises, Celery calls it with the failed task's request (so you get request.id, request.task, request.args), the exception instance and the traceback. The task itself still ends in FAILURE: an errback reports the error, it does not swallow it.
2. Old-style errbacks get only the task id
from common import *
@app.task
def old_style(task_id): # pre-4.0 errback: receives only the parent task id
print('old-style errback got:', task_id[:8])
div.apply_async((1, 0), link_error=old_style.s())
Output:
old-style errback got: 93f5db9c
Celery checks how many parameters the errback takes. With more than one it calls errback(request, exc, traceback); with one, it falls back to the pre-4.0 style and passes just the failed task's id, so old_style receives a UUID string. If your errback gets an id where you expected an exception, fix its signature.
3. Error handling in a chain: on_error()
from common import *
from celery import chain
@app.task
def add_loud(x, y):
print(f'add_loud({x}, {y}) ran'); return x + y
try:
chain(add_loud.s(1, 1), div.s(0), add_loud.s(5)).on_error(on_error.s()).apply_async()
except ZeroDivisionError as e:
print('caller sees:', repr(e)) # eager mode re-raises; with a worker, .get() would
Output:
add_loud(1, 1) ran
errback: task common.div id=0dbf5a03 failed with ZeroDivisionError('division by zero')
caller sees: ZeroDivisionError('division by zero')
chain(...).on_error(errback) attaches the errback to the chain. When div fails, the errback runs with div's request, and the rest of the chain (add_loud(5)) never runs. The caller still sees the original exception: in eager mode apply_async() re-raises it; with a worker, result.get() raises it.
Errbacks run as separate tasks, so with a real worker they need a running worker that has them registered. Keep them small and idempotent: log, alert, or mark a row as failed.
Related: Celery MaxRetriesExceededError, Celery Error 111 Connection refused.
Frequently asked questions
What arguments does a Celery link_error errback receive?
With the signature (request, exc, traceback) it gets the failed task's request, the exception and the traceback. An errback with a single parameter gets only the failed task id.
Does link_error stop the task from failing?
No. The task still ends in FAILURE and the caller still sees the exception; in a chain, later tasks do not run.
Written with AI assistance. Measured on 2026-10-23 with Celery 5.6.3 and Python 3.14.7 (python:3-alpine in Docker, task_always_eager). Each block was run verbatim and the output pasted unedited. PlainHeaders is our extension; Python and Celery are free.