---
name: azion-migrate-handler-patterns-in-functions
description: >-
  Move a function from the legacy Service Worker handler to the ES Modules handler, with the before and after for fetch and firewall.
---

# Migrate handler patterns in Functions

[Functions](/en/documentation/platform/functions/) supports two handler patterns: **ES Modules**, which Azion recommends, and **Service Worker**, which Azion maintains for backward compatibility. Existing Service Worker code keeps running, so the migration is work to schedule rather than a break to repair. Code that follows neither pattern is not supported.

---

## Supported handler patterns

Both patterns give the handler the same request and the same execution context. They differ in how the code receives them.

### ES Modules

An ES Modules function exports a default object. Its `fetch` method receives the request, the environment variables and bindings, and the execution context. A `firewall` method receives the same three arguments:

```javascript
export default {
  fetch: (request, env, ctx) => {
    return new Response('Hello World');
  },
  firewall: (request, env, ctx) => {
    // Blocks the request before it reaches the fetch handler.
    ctx.deny();
  }
};
```

A default export whose `fetch` method takes `request`, `env`, and `ctx` follows this pattern, whether it is an object literal or the application instance a framework returns. An application instance built with a framework qualifies, so a Hono app exported with `export default app` is an ES Modules handler. For a function written that way, refer to [Build a RESTful tasks API with Functions and SQL Database](/en/documentation/guides/application-development/functions-and-runtime/restful-tasks-api-functions/).

### Service Worker

A Service Worker function registers a listener with `addEventListener`. The listener reads the request from an event object, and it answers with `event.respondWith`:

```javascript
addEventListener('fetch', (event) => {
  event.respondWith(handleRequest(event.request));
});

addEventListener('firewall', (event) => {
  // Blocks the request before it reaches the fetch listener.
  event.deny();
});

async function handleRequest(request) {
  return new Response('Hello World');
}
```

---

## Handler parameters

An ES Modules handler receives three arguments. A Service Worker listener receives one event object that carries the same values. For the signature as the runtime reference states it, refer to [Handlers](/en/documentation/devtools/runtime/api-reference/handlers/).

### `fetch(request, env, ctx)` in ES Modules

| Parameter | Type                                                                | Description                                                                                            |
| --------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| `request` | [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) | The incoming HTTP request object                                                                       |
| `env`     | Object                                                              | Environment variables and bindings                                                                     |
| `ctx`     | Object                                                              | Execution context. Use `ctx.waitUntil(promise)` to extend the lifetime of the function for async tasks |

### `firewall(request, env, ctx)` in ES Modules

| Parameter | Type                                                                | Description                                                                                                                                        |
| --------- | ------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `request` | [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) | The incoming HTTP request object                                                                                                                   |
| `env`     | Object                                                              | Environment variables and bindings                                                                                                                 |
| `ctx`     | Object                                                              | Execution context. Call `ctx.deny()` to block the request immediately. If `ctx.deny()` is not called, the request continues to the `fetch` handler |

### The event object in Service Worker

| Property        | Description                                                                                 |
| --------------- | ------------------------------------------------------------------------------------------- |
| `event.request` | Access to the Request object                                                                |
| `event.deny()`  | Blocks the request immediately. If not called, the request continues to the `fetch` handler |

---

## Migrate the handler

The logic stays the same in both patterns. What changes is where the handler reads the request and where it returns the response.

### Migrate the fetch handler

The listener becomes a `fetch` method. The request arrives as the first argument, not as a property of the event.

Service Worker code registers the listener and delegates to a named function:

```javascript
addEventListener('fetch', (event) => {
  event.respondWith(handleRequest(event.request));
});

async function handleRequest(request) {
  const url = new URL(request.url);

  if (url.pathname === '/api/hello') {
    return new Response(JSON.stringify({ message: 'Hello World' }), {
      headers: { 'Content-Type': 'application/json' }
    });
  }

  return new Response('Not Found', { status: 404 });
}
```

The ES Modules replacement moves the same logic into the `fetch` method and returns the response directly:

```javascript
export default {
  fetch: async (request, env, ctx) => {
    const url = new URL(request.url);

    if (url.pathname === '/api/hello') {
      return new Response(JSON.stringify({ message: 'Hello World' }), {
        headers: { 'Content-Type': 'application/json' }
      });
    }

    return new Response('Not Found', { status: 404 });
  }
};
```

### Migrate the firewall handler

Three names change: the listener becomes a `firewall` method, `event.request` becomes `request`, and `event.deny()` becomes `ctx.deny()`.

Service Worker code reads the headers from the event and blocks with `event.deny()`:

```javascript
addEventListener('fetch', (event) => {
  event.respondWith(handleRequest(event.request));
});

addEventListener('firewall', (event) => {
  const clientIP = event.request.headers.get('X-Forwarded-For');
  const userAgent = event.request.headers.get('User-Agent');

  // Blocks bot requests.
  if (userAgent && userAgent.includes('bot')) {
    event.deny();
    return;
  }

  // Blocks one specific address.
  if (clientIP === '192.0.2.100') {
    event.deny();
    return;
  }

  // Without a deny call, the request continues to the fetch listener.
});

async function handleRequest(request) {
  return new Response('Hello World');
}
```

The ES Modules replacement reads the headers from `request` and blocks with `ctx.deny()`:

```javascript
export default {
  fetch: async (request, env, ctx) => {
    return new Response('Access granted');
  },

  firewall: async (request, env, ctx) => {
    const clientIP = request.headers.get('X-Forwarded-For');
    const userAgent = request.headers.get('User-Agent');

    // Blocks bot requests.
    if (userAgent && userAgent.includes('bot')) {
      ctx.deny();
      return;
    }

    // Blocks one specific address.
    if (clientIP === '192.0.2.100') {
      ctx.deny();
      return;
    }

    // Without a deny call, the request continues to the fetch handler.
    return;
  }
};
```

---

## Use the execution context

The `ctx` argument carries the two calls an ES Modules handler makes on the invocation itself.

### Move async work off the response path

`ctx.waitUntil(promise)` extends the lifetime of the function past the response. Pass it the work whose result the response does not need:

```javascript
export default {
  fetch: async (request, env, ctx) => {
    // The response returns without waiting for the log call.
    ctx.waitUntil(logRequest(request));

    return new Response('Hello World');
  }
};

async function logRequest(request) {
  console.log(`Request to: ${request.url}`);
}
```

### Block requests by path

The `firewall` method receives the whole request. The handler can read the URL and apply one rule per path. This handler blocks bots everywhere and restricts `/admin` to a single address range:

```javascript
export default {
  fetch: async (request, env, ctx) => {
    return new Response('Access granted');
  },

  firewall: async (request, env, ctx) => {
    const url = new URL(request.url);
    const userAgent = request.headers.get('User-Agent');
    const clientIP = request.headers.get('X-Forwarded-For');

    // Blocks bot requests.
    if (userAgent && userAgent.includes('bot')) {
      ctx.deny();
      return;
    }

    // Restricts the admin paths to one address range.
    if (url.pathname.startsWith('/admin')) {
      if (!clientIP || !clientIP.startsWith('192.0.2.')) {
        ctx.deny();
        return;
      }
    }

    // Without a deny call, the request continues to the fetch handler.
    return;
  }
};
```

---

## Unsupported patterns

Azion Runtime reports `Unsupported handler pattern detected` when the code matches neither supported pattern. Three shapes produce it:

```javascript
// A function as the default export, with no fetch method.
export default function (request) {
  return new Response('Hello');
}

// A named export instead of the default export.
export function fetch(request) {
  return new Response('Hello');
}

// A handler the file declares and never exports.
function handleRequest(request) {
  return new Response('Hello');
}
```

All three fail the same requirement: the default export must be an object that carries a `fetch` method. Rewrite the code in the ES Modules pattern to clear the error. The Service Worker pattern also clears it, and Azion recommends ES Modules for new code.

For failures that the handler shape does not explain, refer to [Troubleshoot function execution and logs](/en/documentation/platform/functions/troubleshooting/).

---

## Next steps

- [Functions best practices](/en/documentation/platform/functions/best-practices.md): Why ES Modules is the pattern for new code, and the ceilings one invocation runs inside.
- [Run a function on an application](/en/documentation/guides/application-development/functions-and-runtime/serverless-functions.md): Add the Rules Engine rule that runs the instance, from Azion Console or the Azion API.
- [Run a function on a firewall](/en/documentation/guides/application-development/functions-and-runtime/firewall.md): Put a firewall handler behind a firewall rule and block a request with ctx.deny().
- [JavaScript examples](/en/documentation/platform/functions/javascript-examples.md): Handler code to adapt, still written in the Service Worker pattern.
- [Write and test a function](/en/documentation/guides/application-development/functions-and-runtime/first-steps.md): Write the handler in Azion Console and read its response before it serves traffic.
- [Functions](/en/documentation/platform/functions.md): The product reference, with the scope, the limits, and the invocation path.
- [Azion Runtime](/en/documentation/devtools/runtime.md): The Web APIs a handler can call, including Network, Web Streams, and V8 primitives.
- [Troubleshoot function execution and logs](/en/documentation/platform/functions/troubleshooting.md): What to do when a function never runs, stops before it responds, or produces no log output.
