InvalidStateError: Exception is not set

March 5, 2024 Python Asyncio

You can get an InvalidStateError exception when attempting to retrieve an exception from an asyncio Task.

This will happen if we attempt to retrieve an exception from a task while the task is still running, e.g. the task is not yet done.

We can avoid the InvalidStateError exception by waiting for the task to be done before attempting to retrieve the exception either directly or via a function such as asyncio.gather() or asyncio.wait().

In this tutorial, you will discover why we get the InvalidStateError: "Exception is not set" and how to fix it.

Let's get started.

InvalidStateError: Exception is not set

It is common to get an InvalidStateError exception when using asyncio.

An InvalidStateError exception is raised with the message:

Developers often get this exception when first getting started with asyncio.

What does this InvalidStateError exception mean?

How can we fix it and avoid it in the future?

Why Do We Get An InvalidStateError

We get an InvalidStateError exception when we try to retrieve an exception from an asyncio.Task while the task is still running.

This will happen if we call the exception() method on an asyncio.Task instance and the task is not done.

For example:

...
# attempt to get an exception from the task
exc = task.exception()

This is the regular way to get an unhandled exception from a task.

It will return None if the task is completed successfully or an exception instance if an exception was raised in the task.

The problem is, we must wait until the task has been completed, e.g., has the state "done". This means that the done() method on the task returns True.

You can learn more about retrieving exceptions from tasks in the tutorial:

How to Avoid InvalidStateError

We can avoid an InvalidStateError when calling the exception() method by waiting for the task to be done.

There are many ways we can do this, three examples include:

  1. Await the task directly.
  2. Await the task via asyncio.gather()
  3. Await the task via asyncio.wait()

Let's take a closer look at each approach in turn.

Await the Task Directly

The naive approach might be to attempt to await the task directly.

For example:

...
# wait for the task to be done
await task
# attempt to get an exception from the task
exc = task.exception()

This is not helpful because if the task fails with an unhandled exception, it will be propagated to the caller and re-raised.

Therefore we would need to wrap the await in a try-except structure.

For example:

try:
	# wait for the task to be done
	await task
except Exception as e:
	...

We would no longer need to call the exception() method on the task.

Await the Task Via asyncio.gather()

An approach that maintains the same code structure is to use the asyncio.gather() function.

This function takes one or more awaitables and returns once they are all done. We can set the "return_exceptions" argument to True so that any exceptions raised in awaited tasks are handled and returned directly in the list of return values, instead of being propagated to the caller.

For example:

...
# wait for the task to be done
_ = await asyncio.gather(task, return_exceptions=True)
# attempt to get an exception from the task
exc = task.exception()

You can learn more about how to use the asyncio.gather() function in the tutorial:

Await the Task Via asyncio.wait()

Another similar approach is to use the asyncio.wait() function.

This function takes a collection of tasks and returns once a condition is met, where the default condition is that all provided tasks are done.

This means that we will have to enclose our single task in a list.

For example:

...
# wait for the task to be done
_ = await asyncio.wait()
# attempt to get an exception from the task
exc = task.exception()

You can learn more about the asyncio.wait() function in the tutorial:

Example of InvalidStateError: Exception is not set

We can explore an example that raises an InvalidStateError.

In this case, we can define a custom coroutine that does some work and then raises an exception. Our main program will schedule the coroutine as a background task, sleep a moment then attempt to retrieve the exception and report a message if an exception was present.

Firstly, we can define a custom coroutine that reports a message, sleeps for one second, and then fails with an exception.

The work() coroutine below implements this.

# custom coroutine
async def work():
    # report a message
    print('work() is working!')
    # sleep a moment to simulate work
    await asyncio.sleep(1)
    # fails with an exception
    raise Exception('Something bad happened')

Next, we can define the main() coroutine.

A message is printed, then a work() coroutine is created and scheduled as a background task. The main() coroutine then sleeps for half a second, not long enough for the task to be completed.

It then retrieves the exception and if present reports its details, otherwise reports that everything is fine before reporting a final message.

The main() coroutine below implements this.

# main coroutine
async def main():
    # entry point of the program
    print('Main is running')
    # schedule as task
    task = asyncio.create_task(work())
    # wait a moment
    await asyncio.sleep(0.5)
    # check for an exception
    if task.exception():
        # report the exception
        print(f'Failed with: {task.exception()}')
    else:
        print('Everything is fine')
    # report final message
    print('Main is done')

Finally, we can start the event loop.

...
# start the event loop
asyncio.run(main())

Tying this together, the complete example is listed below.

# SuperFastPython.com
# example of InvalidStateError: Exception is not set
import asyncio

# custom coroutine
async def work():
    # report a message
    print('work() is working!')
    # sleep a moment to simulate work
    await asyncio.sleep(1)
    # fails with an exception
    raise Exception('Something bad happened')

# main coroutine
async def main():
    # entry point of the program
    print('Main is running')
    # schedule as task
    task = asyncio.create_task(work())
    # wait a moment
    await asyncio.sleep(0.5)
    # check for an exception
    if task.exception():
        # report the exception
        print(f'Failed with: {task.exception()}')
    else:
        print('Everything is fine')
    # report final message
    print('Main is done')

# start the event loop
asyncio.run(main())

Running the example first starts the asyncio event loop and runs the main() coroutine.

The main() coroutine runs and reports a message.

It then creates the work() coroutine and schedules it as a background task. It then suspends and sleeps for half a second.

The work() task runs and reports a message before suspending and sleeping for one second.

The main() coroutine resumes and attempts to retrieve the exception from the task.

This fails with an InvalidStateError exception and the message "Exception is not set".

This highlights that if we attempt to retrieve an exception via the exception() method before the task is done an InvalidStateError exception will be raised.

Main is running
work() is working!
Traceback (most recent call last):
  File "...", line 32, in <module>
    asyncio.run(main())
  File ".../asyncio/runners.py", line 190, in run
    return runner.run(main)
           ^^^^^^^^^^^^^^^^
  File ".../asyncio/runners.py", line 118, in run
    return self._loop.run_until_complete(task)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File ".../asyncio/base_events.py", line 653, in run_until_complete
    return future.result()
           ^^^^^^^^^^^^^^^
  File "...", line 23, in main
    if task.exception():
       ^^^^^^^^^^^^^^^^
asyncio.exceptions.InvalidStateError: Exception is not set.

Example of Avoiding InvalidStateError By Awaiting The Task

We can explore how to avoid the InvalidStateError exception by awaiting the target task directly.

In this case, we can update the above example to await the task. This requires that we wrap the await expression in a try-except structure. If an exception occurs, we can report the details directly, otherwise report an everything is fine message.

...
try:
    # await the task directly
    await task
except Exception as e:
    # report the exception
    print(f'Failed with: {e}')
else:
    print('Everything is fine')

The updated main() coroutine with this change is listed below.

# main coroutine
async def main():
    # entry point of the program
    print('Main is running')
    # schedule as task
    task = asyncio.create_task(work())
    try:
        # await the task directly
        await task
    except Exception as e:
        # report the exception
        print(f'Failed with: {e}')
    else:
        print('Everything is fine')
    # report final message
    print('Main is done')

Tying this together, the complete example is listed below.

# SuperFastPython.com
# example of avoiding InvalidStateError by awaiting the task directly
import asyncio

# custom coroutine
async def work():
    # report a message
    print('work() is working!')
    # sleep a moment to simulate work
    await asyncio.sleep(1)
    # fails with an exception
    raise Exception('Something bad happened')

# main coroutine
async def main():
    # entry point of the program
    print('Main is running')
    # schedule as task
    task = asyncio.create_task(work())
    try:
        # await the task directly
        await task
    except Exception as e:
        # report the exception
        print(f'Failed with: {e}')
    else:
        print('Everything is fine')
    # report final message
    print('Main is done')

# start the event loop
asyncio.run(main())

Running the example first starts the asyncio event loop and runs the main() coroutine.

The main() coroutine runs and reports a message.

It then creates the work() coroutine and schedules it as a background task. It then suspends and awaits the task directly.

The work() task runs and reports a message before suspending and sleeping for one second.

The work() task then resumes and raises an exception, terminating the task.

The main() coroutine resumes and the exception is propagated. The exception is handled and reported directly.

This highlights how we can avoid an InvalidStateError exception by awaiting the task directly and handling any raised exception.

Main is running
work() is working!
Failed with: Something bad happened
Main is done

Example of Avoiding InvalidStateError By asyncio.gather()

We can explore how to avoid an InvalidStateError exception by awaiting the task with asyncio.gather().

In this case, we can update the example by awaiting a call to asyncio.gather() passing the task instance as an argument, and setting the return_exceptions argument to True.

...
# wait for the task to be done
_ = await asyncio.gather(task, return_exceptions=True)

The benefit of this approach is that we do not have to handle the exception in the caller, it will not be propagated from the asyncio.gather() when the return_exceptions argument is set to True.

The updated main() coroutine with this change is listed below.

# main coroutine
async def main():
    # entry point of the program
    print('Main is running')
    # schedule as task
    task = asyncio.create_task(work())
    # wait for the task to be done
    _ = await asyncio.gather(task, return_exceptions=True)
    # check for an exception
    if task.exception():
        # report the exception
        print(f'Failed with: {task.exception()}')
    else:
        print('Everything is fine')
    # report final message
    print('Main is done')

Tying this together, the complete example is listed below.

# SuperFastPython.com
# example of avoiding InvalidStateError by asyncio.gather()
import asyncio

# custom coroutine
async def work():
    # report a message
    print('work() is working!')
    # sleep a moment to simulate work
    await asyncio.sleep(1)
    # fails with an exception
    raise Exception('Something bad happened')

# main coroutine
async def main():
    # entry point of the program
    print('Main is running')
    # schedule as task
    task = asyncio.create_task(work())
    # wait for the task to be done
    _ = await asyncio.gather(task, return_exceptions=True)
    # check for an exception
    if task.exception():
        # report the exception
        print(f'Failed with: {task.exception()}')
    else:
        print('Everything is fine')
    # report final message
    print('Main is done')

# start the event loop
asyncio.run(main())

Running the example first starts the asyncio event loop and runs the main() coroutine.

The main() coroutine runs and reports a message.

It then creates the work() coroutine and schedules it as a background task.

It then suspends and awaits the task in a call to asyncio.gather().

The work() task runs and reports a message before suspending and sleeping for one second.

The work() task then resumes and raises an exception, terminating the task.

The main() coroutine resumes and checks if the task raised an exception. The task did raise an exception, so the exception details are retrieved and reported directly.

This highlights how we can avoid an InvalidStateError exception by awaiting the task within asyncio.gather().

Main is running
work() is working!
Failed with: Something bad happened
Main is done

Example of Avoiding InvalidStateError By asyncio.wait()

We can explore how to avoid an InvalidStateError exception by awaiting the task with asyncio.wait().

In this case, we can update the example by awaiting a call to asyncio.wait() and passing the task instance as an argument wrapped in a new list object.

...
# wait for the task to be done
_ = await asyncio.wait()

The benefit of this approach is that we do not have to handle the exception in the caller, it will not be propagated from the asyncio.wait() function.

The updated main() coroutine with this change is listed below.

# main coroutine
async def main():
    # entry point of the program
    print('Main is running')
    # schedule as task
    task = asyncio.create_task(work())
    # wait for the task to be done
    _ = await asyncio.wait()
    # check for an exception
    if task.exception():
        # report the exception
        print(f'Failed with: {task.exception()}')
    else:
        print('Everything is fine')
    # report final message
    print('Main is done')

Tying this together, the complete example is listed below.

# SuperFastPython.com
# example of avoiding InvalidStateError by asyncio.wait()
import asyncio

# custom coroutine
async def work():
    # report a message
    print('work() is working!')
    # sleep a moment to simulate work
    await asyncio.sleep(1)
    # fails with an exception
    raise Exception('Something bad happened')

# main coroutine
async def main():
    # entry point of the program
    print('Main is running')
    # schedule as task
    task = asyncio.create_task(work())
    # wait for the task to be done
    _ = await asyncio.wait()
    # check for an exception
    if task.exception():
        # report the exception
        print(f'Failed with: {task.exception()}')
    else:
        print('Everything is fine')
    # report final message
    print('Main is done')

# start the event loop
asyncio.run(main())

Running the example first starts the asyncio event loop and runs the main() coroutine.

The main() coroutine runs and reports a message.

It then creates the work() coroutine and schedules it as a background task.

It then suspends and awaits the task in a call to asyncio.wait() as a one-item list.

The work() task runs and reports a message before suspending and sleeping for one second.

The work() task then resumes and raises an exception, terminating the task.

The main() coroutine resumes and checks if the task raised an exception. The task did raise an exception, so the exception details are retrieved and reported directly.

This highlights how we can avoid an InvalidStateError exception by awaiting the task within asyncio.wait().

Main is running
work() is working!
Failed with: Something bad happened
Main is done

Takeaways

You now know why we get the InvalidStateError: "Exception is not set" and how to fix it.



If you enjoyed this tutorial, you will love my book: Python Asyncio Jump-Start. It covers everything you need to master the topic with hands-on examples and clear explanations.