Migrate from Service Workers to ES Modules
This guide will show you how to migrate your Workers from the Service Worker format to the ES modules format.
Advantages of migrating
There are several reasons to migrate your Workers to the ES modules format:
- Durable Objects, D1, Workers AI, Vectorize and other bindings can only be used from Workers that use ES modules.
- Your Worker will run faster. With service workers, bindings are exposed as globals. This means that for every request, the Workers runtime must create a new JavaScript execution context, which adds overhead and time. Workers written using ES modules can reuse the same execution context across multiple requests.
- You can gradually deploy changes to your Worker when you use the ES modules format.
- You can easily publish Workers using ES modules to
npm
, allowing you to import and reuse Workers within your codebase.
Migrate a Worker
The following example demonstrates a Worker that redirects all incoming requests to a URL with a 301
status code.
With the Service Worker syntax, the example Worker looks like:
async function handler(request) { const base = 'https://example.com'; const statusCode = 301;
const destination = new URL(request.url, base); return Response.redirect(destination.toString(), statusCode);
}
// Initialize Worker
addEventListener('fetch', event => { event.respondWith(handler(event.request));
});
Workers using ES modules format replace the addEventListener
syntax with an object definition, which must be the file’s default export (via export default
). The previous example code becomes:
export default { fetch(request) { const base = 'https://example.com'; const statusCode = 301;
const destination = new URL(request.url, base); return Response.redirect(destination.toString(), statusCode); },
};
Bindings
Bindings allow your Workers to interact with resources on the Cloudflare developer platform.
Workers using ES modules format do not rely on any global bindings. However, Service Worker syntax accesses bindings on the global scope.
To understand bindings, refer the following TODO
KV namespace binding example. To create a TODO
KV namespace binding, you will:
- Create a KV namespace named
My Tasks
and receive an ID that you will use in your binding. - Create a Worker.
- Find your Worker’s
wrangler.toml
configuration file and add a KV namespace binding:
kv_namespaces = [ { binding = "TODO", id = "<ID>" }
]
In the following sections, you will use your binding in Service Worker and ES modules format.
Bindings in Service Worker format
In Service Worker syntax, your TODO
KV namespace binding is defined in the global scope of your Worker. Your TODO
KV namespace binding is available to use anywhere in your Worker application’s code.
index.jsaddEventListener("fetch", async (event) => { return await getTodos()
});
async function getTodos() { // Get the value for the "to-do:123" key // NOTE: Relies on the TODO KV binding that maps to the "My Tasks" namespace. let value = await TODO.get("to-do:123");
// Return the value, as is, for the Response event.respondWith(new Response(value));
}
Bindings in ES modules format
In ES modules format, bindings are only available inside the env
parameter that is provided at the entry point to your Worker.
To access the TODO
KV namespace binding in your Worker code, the env
parameter must be passed from the fetch
handler in your Worker to the getTodos
function.
index.jsimport { getTodos } from './todos'
export default { async fetch(request, env, ctx) { // Passing the env parameter so other functions // can reference the bindings available in the Workers application return await getTodos(env) },
};
The following code represents a getTodos
function that calls the get
function on the TODO
KV binding.
todos.jsasync function getTodos(env) { // NOTE: Relies on the TODO KV binding which has been provided inside of // the env parameter of the `getTodos` function let value = await env.TODO.get("to-do:123"); return new Response(value);
}
export { getTodos }
Environment variables
Environment variables are accessed differently in code written in ES modules format versus Service Worker format.
Review the following example environment variable configuration in wrangler.toml
:
wrangler.tomlname = "my-worker-dev"
# Define top-level environment variables
# under the `[vars]` block using
# the `key = "value"` format
[vars]
API_ACCOUNT_ID = "<EXAMPLE-ACCOUNT-ID>"
Environment variables in Service Worker format
In Service Worker format, the API_ACCOUNT_ID
is defined in the global scope of your Worker application. Your API_ACCOUNT_ID
environment variable is available to use anywhere in your Worker application’s code.
addEventListener("fetch", async (event) => { console.log(API_ACCOUNT_ID) // Logs "<EXAMPLE-ACCOUNT-ID>" return new Response("Hello, world!")
})
Environment variables in ES modules format
In ES modules format, environment variables are only available inside the env
parameter that is provided at the entrypoint to your Worker application.
export default { async fetch(request, env, ctx) { console.log(env.API_ACCOUNT_ID) // Logs "<EXAMPLE-ACCOUNT-ID>" return new Response("Hello, world!") },
};
Cron Triggers
To handle a Cron Trigger event in a Worker written with ES modules syntax, implement a scheduled()
event handler, which is the equivalent of listening for a scheduled
event in Service Worker syntax.
This example code:
addEventListener("scheduled", (event) => { // ...
});
Then becomes:
export default { async scheduled(event, env, ctx) { // ... },
};
Access event
or context
data
Workers often need access to data not in the request
object. For example, sometimes Workers use waitUntil
to delay execution. Workers using ES modules format can access waitUntil
via the context
parameter. Refer to ES modules parameters for more information.
This example code:
async function triggerEvent(event) { // Fetch some data console.log('cron processed', event.scheduledTime);
}
// Initialize Worker
addEventListener('scheduled', event => { event.waitUntil(triggerEvent(event));
});
Then becomes:
async function triggerEvent(event) { // Fetch some data console.log('cron processed', event.scheduledTime);
}
export default { async scheduled(event, env, ctx) { ctx.waitUntil(triggerEvent(event)); },
};
Service Worker syntax
A Worker written in Service Worker syntax consists of two parts:
- An event listener that listens for
FetchEvents
. - An event handler that returns a Response object which is passed to the event’s
.respondWith()
method.
When a request is received on one of Cloudflare’s global network servers for a URL matching a Worker, Cloudflare’s server passes the request to the Workers runtime. This dispatches a FetchEvent
in the isolate where the Worker is running.
~/my-worker/index.jsaddEventListener('fetch', event => { event.respondWith(handleRequest(event.request));
});
async function handleRequest(request) { return new Response('Hello worker!', { headers: { 'content-type': 'text/plain' }, });
}
Below is an example of the request response workflow:
An event listener for the
FetchEvent
tells the script to listen for any request coming to your Worker. The event handler is passed theevent
object, which includesevent.request
, aRequest
object which is a representation of the HTTP request that triggered theFetchEvent
.The call to
.respondWith()
lets the Workers runtime intercept the request in order to send back a custom response (in this example, the plain text'Hello worker!'
).The
FetchEvent
handler typically culminates in a call to the method.respondWith()
with either aResponse
orPromise<Response>
that determines the response.The
FetchEvent
object also provides two other methods to handle unexpected exceptions and operations that may complete after a response is returned.
Learn more about the lifecycle methods of the fetch()
handler.
Supported FetchEvent
properties
event.type
string
- The type of event. This will always return
"fetch"
.
- The type of event. This will always return
event.request
Request
- The incoming HTTP request.
event.respondWith(response
:Response
|Promise)void
- Refer to
respondWith
.
- Refer to
event.waitUntil(promisePromise)
:void
- Refer to
waitUntil
.
- Refer to
event.passThroughOnException()
:void
- Refer to
passThroughOnException
.
- Refer to
respondWith
Intercepts the request and allows the Worker to send a custom response.
If a fetch
event handler does not call respondWith
, the runtime delivers the event to the next registered fetch
event handler. In other words, while not recommended, this means it is possible to add multiple fetch
event handlers within a Worker.
If no fetch
event handler calls respondWith
, then the runtime forwards the request to the origin as if the Worker did not. However, if there is no origin – or the Worker itself is your origin server, which is always true for *.workers.dev
domains – then you must call respondWith
for a valid response.
// Format: Service Worker
addEventListener('fetch', event => { let { pathname } = new URL(event.request.url);
// Allow "/ignore/*" URLs to hit origin if (pathname.startsWith('/ignore/')) return;
// Otherwise, respond with something event.respondWith(handler(event));
});
waitUntil
The waitUntil
command extends the lifetime of the "fetch"
event. It accepts a Promise
-based task which the Workers runtime will execute before the handler terminates but without blocking the response. For example, this is ideal for caching responses or handling logging.
With the Service Worker format, waitUntil
is available within the event
because it is a native FetchEvent
property.
With the ES modules format, waitUntil
is moved and available on the context
parameter object.
service-worker.js// Format: Service Worker
addEventListener('fetch', event => { event.respondWith(handler(event));
});
async function handler(event) { // Forward / Proxy original request let res = await fetch(event.request);
// Add custom header(s) res = new Response(res.body, res); res.headers.set('x-foo', 'bar');
// Cache the response // NOTE: Does NOT block / wait event.waitUntil(caches.default.put(event.request, res.clone()));
// Done return res;
}
passThroughOnException
The passThroughOnException
method prevents a runtime error response when the Worker throws an unhandled exception. Instead, the script will fail open, which will proxy the request to the origin server as though the Worker was never invoked.
To prevent JavaScript errors from causing entire requests to fail on uncaught exceptions, passThroughOnException()
causes the Workers runtime to yield control to the origin server.
With the Service Worker format, passThroughOnException
is added to the FetchEvent
interface, making it available within the event
.
With the ES modules format, passThroughOnException
is available on the context
parameter object.
service-worker.js// Format: Service Worker
addEventListener('fetch', event => { // Proxy to origin on unhandled/uncaught exceptions event.passThroughOnException(); throw new Error('Oops');
});