The fetch() handler

The fetch() handler is the function that a <script type=worker> exports to answer requests. It receives the Request, the worker's env bindings and an execution context, and it returns a Response, or a promise for one. The same handler runs in the playground and, with the edge attribute, on RTEdge.net.

Syntax

js
export default { fetch(request, env, ctx) { /* … */ } }
export default { async fetch(request, env, ctx) { /* … */ } }
export default { fetch: (request, env, ctx) => /* … */ }

The arrow form may be synchronous or async, with an expression or a block body. Whichever form you use, the handler becomes an async function, so it always returns a promise. Keep fetch as the only property of the exported object; see Handler forms.

Parameters

request

A Request with the visitor's scheme, host and query string, and a pathname that depends on how the worker was called. For a : URL, the pathname is the text after the first :, so /:api/users?id=7 reaches the worker as /api/users?id=7 and /: as /. On RTEdge.net, a request that reaches the worker without a : keeps its full original pathname, route prefix included. See Calling the worker.

The platform adds request.rt: the signed-in user, the client IP address, the route and where the code runs. In the playground, requests made with fetch(), XHR or EventSource keep their method, headers and body. A page navigation, such as an iframe or a link, reaches the worker as a bare GET without its method, headers and body; on RTEdge.net navigations keep all three. In the playground, a request body is passed on in Chrome; in browsers whose Request cannot carry the cloned stream, the worker may receive no body.

The playground also provides request.cf, an emulation built from ip.rt.ht that describes the connection of your device, and request.event Deprecated, which shows a toast: use ctx instead. On RTEdge.net, do not rely on request.cf; use request.rt.colo and request.rt.country. request.signal exists on RTEdge.net.

env

The bindings of the worker, exactly { KV, FS }. See env.

ctx

The execution context. In the playground it is the service worker's FetchEvent; on RTEdge.net it is Cloudflare's ExecutionContext, passed through unchanged. ctx.waitUntil(promise) works in both places and keeps the worker alive until the promise settles. ctx.passThroughOnException() does not exist in the playground; use io:fail=open there.

Return value

A Response, or a Promise that resolves to one. Always return a Response; the other results depend on where the code runs:

ResultPlaygroundRTEdge.net
A ResponseServed to the page.Served. The status, status text and headers are copied, and an edge-worker header is added.
undefinedThe request falls through. A request to an io:host host goes to the network unchanged. For the output origin, a non-GET request to / gets your page, and other paths go to the network.A 501 page: Worker response is undefined.
A string or another value that is not a ResponseA network error.A TypeError, which produces the 500 exception page.

Do not return the statuses 598 or 599. The platform uses them as internal signals between the worker and the router.

Exceptions

What a visitor sees when the handler fails:

FailurePlaygroundRTEdge.net
The promise rejects with an ErrorWith io:fail=closed, the default: HTTP 500, text/plain, with a filtered stack. With io:fail=open: the original request goes to the network.HTTP 500, an HTML page titled Exception that says Worker encountered an exception. and shows the ray ID, the location, the time and the first two stack lines.
A synchronous throw, a syntax or compile error, an error at the top level, or a rejection with something that is not an ErrorClosed: a network error, because fetch() rejects with a TypeError and a navigation shows the browser's error page. Open: the request goes to the network.A synchronous throw gives the same 500 page.

In the playground, failures are also logged to the playground console, prefixed 🛎️⚙️: ☁️ marks the closed mode and ⛅ the open mode, followed by a stack. Throw Error objects, because other values are not logged reliably. The line numbers in a playground stack are not editor line numbers; treat them as approximate. On RTEdge.net the code is parsed and printed again at deploy, so line and column numbers refer to the compiled script.

A failure outside the handler, such as an exceeded resource limit, does not produce the exception page. See Error pages and the edge worker runtime.

Usage notes

Handler forms

Both runtimes read the handler with the same transform, which rewrites export default { fetch … } as a module-scope async function fetch(…). A module-scope function fetch declaration without a default export works too. What the transform does not handle:

Write async before a handler that uses await. The playground's transform also accepts await inside a non-async arrow handler Experimental, because it wraps the body in an async function, but that is not valid JavaScript and you should not rely on it.

Module scope and state

The legacy fetch attribute Deprecated

Warning: The fetch= attribute is deprecated, works in the playground only, and probably does not survive deployment. Use export default { fetch }.

Without import or export, a worker is a classic script, and the handler is the top-level binding named by the fetch attribute of the first worker script, response by default. A handler of this kind must return a promise: a Response returned synchronously is ignored when io:fail is closed. If the attribute value ends in an underscore, as in fetch=handle_, the worker answers every host, like io:host=*.

html
<script type=worker fetch=handle_>
  const handle_ = async request => new Response(`legacy: ${request.url}`);
</script>

The modern equivalent is <script type=worker io:host=*> with export default { fetch }, returning undefined for the requests you do not handle.

Examples

A JSON API on : paths

The worker routes on the pathname and the query string, and answers unknown paths with a JSON 404.

html
<pre id=out></pre>
<script type=module>
  const get = path => fetch(path).then(r => r.json())
  out.textContent = JSON.stringify([await get(':api/items?limit=2'), await get(':api/missing')], null, 2)
</script>
<script type=worker edge>
  const items = ['apple', 'banana', 'cherry']
  export default {
    fetch: async request => {
      const { pathname, searchParams } = new URL(request.url)
      if (pathname === '/api/items') return Response.json(items.slice(0, Number(searchParams.get('limit') ?? items.length)))
      return Response.json({ error: 'Not found', path: pathname }, { status: 404 })
    }
  }
</script>

Background work with ctx.waitUntil()

The worker answers at once and finishes the write in the background. It only records visits by signed-in users.

html
<iframe src=:></iframe>
<script type=worker edge>
  export default {
    fetch: async (request, { KV }, ctx) => {
      if (request.rt.user) ctx.waitUntil(KV.open({ user: request.rt.user }).put('last', request.url))
      return new Response('logged')
    }
  }
</script>

Handling your own errors

Catch failures and answer with your own JSON instead of the generic error page. In the playground, console.error() in worker code appears in the playground console with the 🛎️⚙️ prefix.

html
<iframe src=:></iframe>
<script type=worker edge>
  const handle = async request => { throw new Error('boom') }
  export default {
    fetch: async (request, env, ctx) => {
      try { return await handle(request, env, ctx) }
      catch (e) { console.error(e); return Response.json({ error: e.message }, { status: 500 }) }
    }
  }
</script>

Specifications

Not part of any specification. The handler follows the Cloudflare Workers fetch() handler. In the playground it is called from the fetch event of a service worker, with the Fetch Standard Request and Response types.

Availability

FeaturePlaygroundRTEdge.netNotes
export default { fetch }YesYesThe same transform in both. Deploy with the edge attribute.
ctx.waitUntil()YesYes
request.rtYesYesFields differ.
request.cfYesNoAn emulation in the playground. On RTEdge.net, use request.rt.colo and country instead.
request.eventYesNoDeprecated.
fetch= attributeYesNoDeprecated.
io:failYesNoPlayground only.

See also