Why does Celery NOT throw an Exception when the underlying task throws one

Question:

Celery doesn’t seem to be handling exceptions properly.

If I have task:

def errorTest():
    raise Exception()

and then I call

r = errorTest.delay()
In [8]: r.result

In [9]: r.state
Out[9]: 'PENDING'

And it will hang like this indefinitely.

Going and checking the logs shows that the error IS getting thrown in the task (and if you want the message, ask), and I know that the backend and everything is set up properly because other tasks just work and return results correctly.

Is there something funky that I need to do to catch exceptions in Celery?

/Celery version is 3.0.13, broker is RabbitMQ running on my local machine

Asked By: Kevin Meyer

||

Answers:

You can define an on_failure function in your Task subclass to handle them correctly. If you’re just looking to find out what happened you can setup error email notifications that will send you the stack trace in your celery config.

Note: As of v4 Celery no longer supports sending emails.

Answered By: timeartist

Going to make @primalpython’s answer more explicit.

This will fail:

@task
def error():
    raise Exception

Input/Output:

In [7]: r = error.delay()

In [8]: print r.state
Out[8]: 'PENDING'

In [9]: print r.result
Out[9]: None

This will succeed:

@task
def error():
    raise Exception

    def on_failure(self, *args, **kwargs):
        pass

Input/Output:

In [7]: r = error.delay()

In [8]: print r.state
Out[8]: 'FAILURE'

In [9]: print r.result
Out[9]: Exception()
Answered By: Kevin Meyer

If you are running Celery with the CELERY_ALWAYS_EAGER set to True, then make sure you include this line in your settings too:

CELERY_EAGER_PROPAGATES_EXCEPTIONS = True

http://docs.celeryproject.org/en/latest/configuration.html#celery-eager-propagates-exceptions

Answered By: seddonym

IMO the easiest way to do this is to pass in a reference to a task class to use when you create your new Celery application.

In one module define the task class to use by default:

from celery.app.task import Task
import logging

logger=logging.getLogger(__name__)

class LoggingTask(Task):
  def on_failure(self, exc, task_id, args, kwargs, einfo):
      kwargs={}
      if logger.isEnabledFor(logging.DEBUG):
         kwargs['exc_info']=exc
      logger.error('Task % failed to execute', task_id, **kwargs)
      super().on_failure(exc, task_id, args, kwargs, einfo)

When you define your app, reference the module (note, it’s a string reference you provide..):

from celery import Celery

app=Celery('my_project_name', task_cls='task_package.module_name:LoggingTask')

From that point forward, if no task class is specifically provided, the LoggingTask will be used – thereby allowing you to effect all existing tasks (that use the default) rather than having to modify each one. This also means you can use the @shared_task decorator as normal..

Answered By: chander