# Lifecycle

> Understand how Nitro runs and serves incoming requests to your application.

## Request lifecycle

A request can be intercepted and terminated (with or without a response) from any of these layers, in this order:

<steps>

### `request` hook

The `request` hook is the first code that runs for every incoming request. It is registered via a [runtime plugin](/docs/plugins):

```ts [plugins/request-hook.ts]
import { definePlugin } from "nitro";

export default definePlugin((nitroApp) => {
  nitroApp.hooks.hook("request", (event) => {
    console.log(`Incoming request on ${event.req.url}`);
  });
});
```

<note>

Errors thrown inside the `request` hook are captured by the [`error` hook](#error-handling) and do not terminate the request pipeline.
</note>

### Route rules

Matching [route rules](/docs/routing#route-rules) from the Nitro config execute next. Route rules run as middleware, before any global middleware, and most of them alter the response without terminating it (for instance, adding a header or setting a cache policy). The matched rules are resolved before any middleware runs and are available as `event.context.routeRules` in every middleware.

```ts [nitro.config.ts]
import { defineConfig } from "nitro";

export default defineConfig({
  routeRules: {
    '/**': { headers: { 'x-nitro': 'first' } }
  }
})
```

<read-more></read-more>

### Static assets

When static asset serving is enabled (the default for most presets), Nitro registers a static assets handler as the **first global middleware**. It checks if the request matches a file in the `public/` directory before other global middleware and route handlers run.

If a match is found, the static file is served immediately with appropriate `Content-Type`, `ETag`, `Last-Modified`, and `Cache-Control` headers. The request is terminated and no further middleware or routes are executed.

Static assets also support content negotiation for pre-compressed files (gzip, brotli, zstd) via the `Accept-Encoding` header.

### Global middleware

Any global middleware defined in the `middleware/` directory runs next:

```ts [middleware/info.ts]
import { defineHandler } from "nitro";

export default defineHandler((event) => {
  event.context.info = { name: "Nitro" };
});
```

<warning>

Returning from a middleware will close the request and should be avoided when possible.
</warning>

<read-more>

Learn more about Nitro middleware.
</read-more>

### Routed middleware

Middleware registered for a specific route pattern (via the [`handlers` config](/docs/routing#route-scoped-middleware)) runs after global middleware. Any middleware attached to the matched route itself runs last, right before the route handler.

### Routes

Nitro matches the incoming request against the routes defined in the `routes/` folder.

```ts [routes/api/hello.ts]
export default (event) => ({ world: true })
```

<read-more>

Learn more about Nitro filesystem routing.
</read-more>

### Server entry

If a server entry is defined, it catches all requests not matched by any route, acting as a `/**` route handler.

```ts [server.ts]
import { defineHandler } from "nitro";

export default defineHandler((event) => {
  if (event.url.pathname === "/") {
    return "Home page";
  }
});
```

<read-more>

Learn more about the Nitro server entry.
</read-more>

### Renderer

If no route is matched, Nitro will look for a renderer handler (defined or auto-detected) to handle the request.

<read-more>

Learn more about Nitro renderer.
</read-more>

### `response` hook

After the response is created (from any of the layers above), the `response` hook runs. This hook receives the final `Response` object and the event, and can be used to inspect or modify response headers:

```ts [plugins/response-hook.ts]
import { definePlugin } from "nitro";

export default definePlugin((nitroApp) => {
  nitroApp.hooks.hook("response", (res, event) => {
    console.log(`Response ${res.status} for ${event.req.url}`);
  });
});
```

<note>

The `response` hook runs for every response, including static assets, middleware-terminated requests, and error responses.
</note>
</steps>

## Error handling

When an error occurs at any point in the request lifecycle, Nitro:

1. Calls the `error` hook with the error and context (including the event and source tags).
2. Passes the error to the **error handler** which converts it into an HTTP response.

### Throwing errors
Throw an `HTTPError` from any handler or middleware to end the request with a specific status code, message, and optional data:

```ts [routes/signup.ts]
import { defineHandler, HTTPError } from "nitro";

export default defineHandler((event) => {
  // Message and details
  throw new HTTPError("Invalid user input", { status: 400 });

  // Status code shortcut
  throw HTTPError.status(400, "Bad Request");

  // Full object form
  throw new HTTPError({
    status: 400,
    message: "Invalid user input",
    data: { field: "email" },
  });
});
```

<note>

Any other thrown value (such as a plain `new Error()`) is treated as an **unhandled** error: the response is always `500` and the message, data, and stack are hidden from the client for security.
</note>

### Default error responses

In production, the default error handler always responds with JSON:

```json
{
  "error": true,
  "status": 400,
  "message": "Invalid user input",
  "data": { "field": "email" }
}
```

In development, programmatic requests receive the same JSON format, but when the request's `Accept` header includes `text/html` (browsers), Nitro renders an interactive HTML error page with a source-mapped stack trace.

### Custom error handlers

To control error response formatting, set `errorHandler` in the Nitro config to a file that exports a handler with `defineErrorHandler`:

```ts [nitro.config.ts]
import { defineConfig } from "nitro";

export default defineConfig({
  errorHandler: "~/error",
});
```

```ts [error.ts]
import { defineErrorHandler } from "nitro";

export default defineErrorHandler((error, event) => {
  return new Response(`[custom error] ${error.message}`, {
    status: error.status || 500,
    headers: { "Content-Type": "text/plain" },
  });
});
```

Return a `Response` to send it to the client, or return nothing to fall through to the next handler. `errorHandler` also accepts an array of handlers that run in order: the first one that returns a response wins, and the built-in default handler is always appended as the last entry, so there is always a fallback response:

```ts [nitro.config.ts]
import { defineConfig } from "nitro";

export default defineConfig({
  errorHandler: ["~/error/known-errors", "~/error/fallback"],
});
```

If a handler itself throws, the failure is logged and the chain continues with the next handler. Each handler also receives the built-in `defaultHandler` as `{ defaultHandler }` in its third argument, so you can reuse the default rendering within your own logic.

<read-more>

See a full custom error handler example.
</read-more>

To customize error rendering for the development server only (without affecting the production bundle), pass a handler function as `devErrorHandler`:

```ts [nitro.config.ts]
import { defineConfig } from "nitro";
import errorHandler from "./error.ts";

export default defineConfig({
  devErrorHandler: errorHandler,
});
```

### Observing errors

To observe errors without changing the response, use the `error` runtime hook from a [plugin](/docs/plugins):

```ts [plugins/errors.ts]
import { definePlugin } from "nitro";

export default definePlugin((nitroApp) => {
  nitroApp.hooks.hook("error", (error, context) => {
    console.error("Captured error:", error);
    // context.event - the H3 event (if available)
    // context.tags  - error source tags like "request", "response", "plugin"
  });
});
```

You can also feed errors into the same pipeline programmatically with [`nitroApp.captureError`](/docs/plugins#programmatic-error-capture). Errors are also tracked per-request in `event.req.context.nitro.errors` for inspection in later hooks.

Additionally, unhandled promise rejections and uncaught exceptions at the process level are automatically captured into the `error` hook with the tags `"unhandledRejection"` and `"uncaughtException"`.

## Server shutdown

When the Nitro server is shutting down, the `close` hook is called. Use this to clean up resources such as database connections, timers, or external service handles:

```ts [plugins/cleanup.ts]
import { definePlugin } from "nitro";

export default definePlugin((nitroApp) => {
  nitroApp.hooks.hook("close", async () => {
    // Clean up resources
  });
});
```

<note>

The hook is awaited after the HTTP server stops accepting connections. Long-running server
presets (Node.js, Bun and Deno) call it on `SIGINT` and `SIGTERM`. Serverless and edge presets
have no shutdown signal, so the hook only runs there if the platform closes the runtime.
</note>

## Hooks reference

All runtime hooks are registered through [runtime plugins](/docs/plugins) using `nitroApp.hooks.hook()`.

| Hook | Signature | When it runs |
| --- | --- | --- |
| `request` | `(event: HTTPEvent) => void \| Promise<void>` | Start of each request, before routing. |
| `response` | `(res: Response, event: HTTPEvent) => void \| Promise<void>` | After the response is created, before it is sent. |
| `error` | `(error: Error, context: { event?, tags? }) => void` | When any error is captured during the lifecycle. |
| `close` | `() => void` | When the Nitro server is shutting down. |

<note>

The `NitroRuntimeHooks` interface is augmentable. Deployment presets (such as Cloudflare) can extend it with platform-specific hooks.
</note>

<read-more>

Learn more about Nitro plugins and hook usage examples.
</read-more>
