Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

The warning means an async def function was called without being awaited or scheduled. With Pyppeteer, the usual trigger is registering an asynchronous request handler directly with page.on('request', block_image). The event emitter invokes the callback like a normal function, producing a coroutine object that never runs. Register a regular callback that schedules the asynchronous handler, then await either request.abort() or request.continue_() for every intercepted request.

import asyncio

async def block_image(request):
    if request.url.endswith(('.png', '.jpg')):
        await request.abort()
    else:
        await request.continue_()

await page.setRequestInterception(True)
page.on('request', lambda request: asyncio.create_task(block_image(request)))

On Python versions before 3.7, use asyncio.ensure_future(...) instead of asyncio.create_task(...). Also inspect the surrounding call chain: the same warning can come from another asynchronous method called without await.

What “coroutine was never awaited” actually means

Calling an async def function does not execute its body immediately. It creates a coroutine object. That object must be awaited by another coroutine or scheduled as a task on a running event loop. If it is discarded, Python eventually emits RuntimeWarning: coroutine 'block_image' was never awaited.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The warning identifies the coroutine that was created, not necessarily the line where the mistake first became visible. Read the name in the warning, search for every call to that function, and then inspect any event registration or wrapper around it.

Python’s asyncio development guidance gives the usual remedy: “The usual fix is to either await the coroutine or call the asyncio.create_task() function.” Which option is correct depends on the caller.

Why Pyppeteer request handlers trigger the warning

The callback mismatch

Pyppeteer’s page.on() event interface expects a callback. A direct registration such as this one passes an asynchronous function to an emitter that does not automatically await it:

page.on('request', block_image)

When a request arrives, Pyppeteer calls block_image(request). Because block_image is asynchronous, that call returns a coroutine object. Unless something schedules or awaits it, the handler body never reaches request.abort() or request.continue_().

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The safe bridge between the event and the coroutine

Use a synchronous callback to create a task:

page.on('request', lambda request: asyncio.create_task(block_image(request)))

The lambda runs synchronously when the event fires and hands the coroutine to the active event loop. The handler remains asynchronous, so it can await Pyppeteer’s request methods normally.

Complete working pattern

The following example launches a browser, enables interception before navigation, blocks common image extensions, and allows every other request to continue. It uses Python 3.7 or newer.

import asyncio
from pyppeteer import launch

IMAGE_SUFFIXES = ('.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg')

async def block_images(request):
    try:
        if request.url.lower().split('?', 1)[0].endswith(IMAGE_SUFFIXES):
            await request.abort()
        else:
            await request.continue_()
    except Exception as exc:
        # Log the URL and exception so task failures are visible.
        print(f'Request handling failed for {request.url}: {exc!r}')

async def main():
    browser = await launch()
    page = await browser.newPage()

    # This is a coroutine and must be awaited before navigation.
    await page.setRequestInterception(True)

    # page.on expects a regular callback; create_task schedules the async work.
    page.on('request', lambda request: asyncio.create_task(block_images(request)))

    await page.goto('https://example.com', {'waitUntil': 'networkidle2'})
    print(await page.title())
    await browser.close()

if __name__ == '__main__':
    asyncio.run(main())

The URL normalization in this example prevents a query string from defeating a suffix check. For production filtering, inspect the request resource type or use a URL parser rather than relying only on file extensions.

Python versions before 3.7

asyncio.create_task() was added in Python 3.7. On an older supported interpreter, use:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
page.on('request', lambda request: asyncio.ensure_future(block_images(request)))

Pyppeteer’s own asynchronous event-handler example uses asyncio.ensure_future, which is why that spelling appears in older examples.

Request interception requirements

Enable interception before the request you want to control

Call and await:

await page.setRequestInterception(True)

Do this before page.goto() or any action that starts the requests you intend to inspect. Interception enables the Request control methods, including abort(), continue_(), and response().

Resolve every intercepted request

An intercepted request must receive an action. A blocked resource normally needs:

await request.abort()

An allowed resource normally needs:

await request.continue_()

Both are coroutines. Omitting await on either call creates another unscheduled coroutine and can leave the page waiting indefinitely. Do not write a handler that filters only the blocked branch and forgets to continue all other requests.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Choose the action deliberately

  • Abort: use when the resource should not load, such as an image, advertisement, or known tracker.
  • Continue: use for requests that should proceed normally.
  • Respond: use only when you intentionally provide a synthetic response and have implemented that flow for your installed Pyppeteer version.

Await directly or schedule a task?

Situation Correct pattern Why
You are already inside an async def and need the result before continuing result = await operation() The current coroutine pauses until the operation finishes and can catch its exception directly.
A regular event callback must start asynchronous work asyncio.create_task(operation()) The synchronous callback hands the coroutine to the running loop.
Python older than 3.7 asyncio.ensure_future(operation()) This is the compatible task-scheduling API used by older Pyppeteer examples.

Scheduling is not the same as completing. A task can still fail after the callback returns, so decide how your program will observe exceptions and know when all request work is finished.

Find other unawaited calls in the same program

The reported Pyppeteer case included another defect: an asynchronous REQUESTER.proxy_browser_request(...) method was called from a regular get_request function without await. Fixing the event registration alone would not make that operation run.

Search by the warning name

  1. Copy the coroutine name from the warning, for example block_image.
  2. Search for every call site, including wrappers, lambdas, callbacks, and error paths.
  3. For each call, check whether the caller is inside async def. If it is, use await when the result is needed.
  4. If the caller is a normal callback, schedule the coroutine with create_task or ensure_future.
  5. Repeat the search for every asynchronous method called by that function.

Recognize common mistakes

# Wrong inside an async function: coroutine is created and discarded
REQUESTER.proxy_browser_request(url)

# Right when completion is required
await REQUESTER.proxy_browser_request(url)

# Right from a synchronous callback, if background execution is intentional
asyncio.create_task(REQUESTER.proxy_browser_request(url))

Do not “fix” the warning by filtering it out. Suppressing the warning can hide a handler that never aborts or continues requests.

Task lifetime, exceptions, and shutdown

A fire-and-forget task may outlive the code that created it. Keep references when you need reliable completion or diagnostics:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
pending = set()

def schedule_request(request):
    task = asyncio.create_task(block_images(request))
    pending.add(task)
    task.add_done_callback(pending.discard)
    task.add_done_callback(report_task_error)

def report_task_error(task):
    if not task.cancelled():
        error = task.exception()
        if error is not None:
            print(f'request task failed: {error!r}')

page.on('request', schedule_request)

At shutdown, wait for the tasks you still care about before closing the browser:

if pending:
    await asyncio.gather(*pending, return_exceptions=True)
await browser.close()

Only use this pattern when your application owns the task set. If requests can continue indefinitely, cancel outstanding tasks as part of shutdown and retrieve their exceptions so they do not surface later as unhandled-task messages.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Troubleshooting checklist

The warning still names block_image

  • Look for a second registration such as page.on('request', block_image) in another function.
  • Check that a wrapper itself is not calling block_image(request) and discarding the returned coroutine.
  • Search tests, retry handlers, and exception branches for direct calls.

Requests hang after interception is enabled

  • Confirm await page.setRequestInterception(True) runs before navigation.
  • Ensure every branch calls exactly one request action.
  • Check that the task is scheduled on the same running event loop as Pyppeteer.
  • Inspect task exceptions; a failed handler can prevent a request from being resolved.

RuntimeError: no running event loop

asyncio.create_task() requires a running loop. Register the handler while inside the coroutine that owns the Pyppeteer page, or use the loop-aware scheduling approach appropriate to your application. Do not create tasks at module import time.

The code works on one Python version but not another

Check the interpreter version. Use create_task on Python 3.7 and newer; use ensure_future on older Python versions. Also verify the installed Pyppeteer release. The API reference consulted for this behavior is version 0.0.25, and exact details can differ in other releases.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Only some resources are blocked

Log request.url and the request type, then test URLs with query strings, uppercase extensions, redirects, and URLs whose content type does not match the suffix. A suffix rule is a convenience filter, not a complete content classifier.

Version and compatibility notes

The Pyppeteer API reference for this guidance is explicitly version 0.0.25. Confirm your installed version before copying examples verbatim. The core asyncio rule is stable: an asynchronous function must be awaited or scheduled, and Pyppeteer’s interception setup and request actions are asynchronous operations.

Also confirm your Python version before choosing the task API. Modern code should prefer asyncio.create_task; legacy environments may require asyncio.ensure_future.

Or skip the browser setup

If your goal is simply to obtain a clean screenshot rather than build a custom interception pipeline, ScreenshotNeo provides a website screenshot API and MCP server. It accepts consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed as clean shots, and each response identifies the result with X-Page-Verdict and X-Billed headers.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use the API documentation at https://screenshotneo.com/docs/ for the complete option set. A one-call cURL example is:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Python:

import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)

Node.js:

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

ScreenshotNeo also offers an MCP server with take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. It supports full-page and element captures, device presets, retina scale, PDF controls, custom CSS and JavaScript, clicks, waits, request blocking, headers, cookies, geolocation, caching, signed links, asynchronous webhooks, bulk capture, and a usage API.

The Free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 shots, with every feature available on every plan. Create a free ScreenshotNeo account to try it without adding a card.

Frequently Asked Questions

Can I make the event handler itself synchronous?

Yes, but it still must schedule the asynchronous request action. A synchronous handler that calls request.abort() or request.continue_() without scheduling or awaiting those coroutines creates the same problem.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Why does the warning appear when the program exits?

Python often reports an unawaited coroutine when it is garbage-collected, which may happen well after the original callback ran. The delayed timing does not mean the exit code caused the bug.

Should I use asyncio.run() inside the request callback?

No. The callback already runs in an application with an event loop. Schedule the handler on that loop instead of trying to start a nested event loop.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.