Workers Best Practices·续2
const result = await env.MY_KV.get(url.pathname); return new Response(result ?? "Not found", { status: result ? 200 : 404, }); } catch (e) { // ✅ Good: console.error appears as "error" severity in Workers Observability console.error( JSON.stringify({ message: "request failed", error: e instanceof Error ? e.message : String(e), path: url.pathname, }), ); return Response.json({ error: "Internal server error" }, { status: 500 }); } }, };
// 🔴 Bad: unstructured string logs are hard to query const badHandler = { async fetch(request, env) { const url = new URL(request.url); console.log("Got a request to " + url.pathname); return new Response("OK"); }, };
*src/index.tsts*
```ts
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
try {
// ✅ Good: structured JSON — searchable and filterable in the dashboard
console.log(
JSON.stringify({
message: "incoming request",
method: request.method,
path: url.pathname,
}),
);
const result = await env.MY_KV.get(url.pathname);
return new Response(result ?? "Not found", {
status: result ? 200 : 404,
});
} catch (e) {
// ✅ Good: console.error appears as "error" severity in Workers Observability
console.error(
JSON.stringify({
message: "request failed",
error: e instanceof Error ? e.message : String(e),
path: url.pathname,
}),
);
return Response.json({ error: "Internal server error" }, { status: 500 });
}
},
} satisfies ExportedHandler<Env>;
// 🔴 Bad: unstructured string logs are hard to query
const badHandler = {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
console.log("Got a request to " + url.pathname);
return new Response("OK");
},
} satisfies ExportedHandler<Env>;
For more information, refer to Workers Logs and Traces.
For more information on all available observability tools, refer to Workers Observability.
Code patterns
Do not store request-scoped state in global scope
Workers reuse isolates across requests. A variable set during one request is still present during the next. This causes cross-request data leaks, stale state, and "Cannot perform I/O on behalf of a different request" errors.
Pass state through function arguments or store it on env bindings. Never in module-level variables.
src/index.jsjs
// 🔴 Bad: global mutable state leaks between requests
let currentUser = null;
const badHandler = {
async fetch(request, env, ctx) {
// Storing request-scoped data globally means the next request sees stale data
currentUser = request.headers.get("X-User-Id");
const result = await handleRequest(currentUser, env);
return Response.json(result);
},
};
// ✅ Good: pass request-scoped data through function arguments
export default {
async fetch(request, env, ctx) {
const userId = request.headers.get("X-User-Id");
const result = await handleRequest(userId, env);
return Response.json(result);
},
};
async function handleRequest(userId, env) {
return { userId };
}
src/index.tsts
// 🔴 Bad: global mutable state leaks between requests
let currentUser: string | null = null;
const badHandler = {
async fetch(
request: Request,
env: Env,
ctx: ExecutionContext,
): Promise<Response> {
// Storing request-scoped data globally means the next request sees stale data
currentUser = request.headers.get("X-User-Id");
const result = await handleRequest(currentUser, env);
return Response.json(result);
},
} satisfies ExportedHandler<Env>;
// ✅ Good: pass request-scoped data through function arguments
export default {
async fetch(
request: Request,
env: Env,
ctx: ExecutionContext,
): Promise<Response> {
const userId = request.headers.get("X-User-Id");
const result = await handleRequest(userId, env);
return Response.json(result);
},
} satisfies ExportedHandler<Env>;
async function handleRequest(userId: string | null, env: Env): Promise<object> {
return { userId };
}
For more information, refer to Workers errors.
Always await or waitUntil your Promises
A Promise that is not awaited, returned, or passed to ctx.waitUntil() is a floating promise. Floating promises cause silent bugs: dropped results, swallowed errors, and unfinished work. The Workers runtime may terminate your isolate before a floating promise completes.
Choose based on whether the response depends on the work. Use await or return for work that must complete before the response is correct. Use ctx.waitUntil() for work that can run after the response is sent and can finish within the waitUntil() time limit.
Enable the no-floating-promises lint rule to catch these at development time. If you use ESLint, enable @typescript-eslint/no-floating-promises ↗︎. If you use oxlint, enable typescript/no-floating-promises ↗︎.
# ESLint (typescript-eslint)
npx eslint --rule '{"@typescript-eslint/no-floating-promises": "error"}' src/
# oxlint
npx oxlint --deny typescript/no-floating-promises src/
src/index.jsjs
export default {
async fetch(request, env, ctx) {
const data = await request.json();
// 🔴 Bad: floating promise — result is dropped, errors are swallowed
fetch("https://api.example.com/webhook", {
method: "POST",
body: JSON.stringify(data),
});
// ✅ Good: await if you need the result before responding
const response = await fetch("https://api.example.com/process", {
method: "POST",
body: JSON.stringify(data),
});
// ✅ Good: waitUntil if you do not need the result before responding
ctx.waitUntil(
fetch("https://api.example.com/webhook", {
method: "POST",
body: JSON.stringify(data),
}),
);
return new Response("OK");
},
};
src/index.tsts
export default {
async fetch(
request: Request,
env: Env,
ctx: ExecutionContext,
): Promise<Response> {
const data = await request.json();
// 🔴 Bad: floating promise — result is dropped, errors are swallowed
fetch("https://api.example.com/webhook", {
method: "POST",
body: JSON.stringify(data),
});
// ✅ Good: await if you need the result before responding
const response = await fetch("https://api.example.com/process", {
method: "POST",
body: JSON.stringify(data),
});
// ✅ Good: waitUntil if you do not need the result before responding
ctx.waitUntil(
fetch("https://api.example.com/webhook", {
method: "POST",
body: JSON.stringify(data),
}),
);
return new Response("OK");
},
} satisfies ExportedHandler<Env>;
Security
Use Web Crypto for secure token generation
The Workers runtime provides the Web Crypto API for cryptographic operations. Use crypto.randomUUID() for unique identifiers and crypto.getRandomValues() for random bytes. Never use Math.random() for anything security-sensitive. It is not cryptographically secure.
Node.js node:crypto is also fully supported when nodejs_compat is enabled, so you can use whichever API you or your libraries prefer.
src/index.jsjs
export default {
async fetch(request, env) {
// 🔴 Bad: Math.random() is predictable and not suitable for security
const badToken = Math.random().toString(36).substring(2);
// ✅ Good: cryptographically secure random UUID
const sessionId = crypto.randomUUID();
// ✅ Good: cryptographically secure random bytes for tokens
const tokenBytes = new Uint8Array(32);
crypto.getRandomValues(tokenBytes);
const token = Array.from(tokenBytes)
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
return Response.json({ sessionId, token });
},
};
src/index.tsts
export default {
async fetch(request: Request, env: Env): Promise<Response> {
// 🔴 Bad: Math.random() is predictable and not suitable for security
const badToken = Math.random().toString(36).substring(2);
// ✅ Good: cryptographically secure random UUID
const sessionId = crypto.randomUUID();
// ✅ Good: cryptographically secure random bytes for tokens
const tokenBytes = new Uint8Array(32);
crypto.getRandomValues(tokenBytes);
const token = Array.from(tokenBytes)
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
return Response.json({ sessionId, token });
},
} satisfies ExportedHandler<Env>;
When comparing secret values (API keys, tokens, HMAC signatures), use crypto.subtle.timingSafeEqual() to prevent timing side-channel attacks. Do not short-circuit on length mismatch. Encode both values to a fixed-size hash first.
src/verify.jsjs
async function verifyToken(provided, expected) {
const encoder = new TextEncoder();
// ✅ Good: hash both values to a fixed size, then compare in constant time
// This avoids leaking the length of the expected value
const [providedHash, expectedHash] = await Promise.all([
crypto.subtle.digest("SHA-256", encoder.encode(provided)),
crypto.subtle.digest("SHA-256", encoder.encode(expected)),
]);
return crypto.subtle.timingSafeEqual(providedHash, expectedHash);
}
// 🔴 Bad: direct string comparison leaks timing information
function verifyTokenInsecure(provided, expected) {
return provided === expected;
}
src/verify.tsts
async function verifyToken(
provided: string,
expected: string,
): Promise<boolean> {
const encoder = new TextEncoder();
// ✅ Good: hash both values to a fixed size, then compare in constant time
// This avoids leaking the length of the expected value
const [providedHash, expectedHash] = await Promise.all([
crypto.subtle.digest("SHA-256", encoder.encode(provided)),
crypto.subtle.digest("SHA-256", encoder.encode(expected)),
]);
return crypto.subtle.timingSafeEqual(providedHash, expectedHash);
}
// 🔴 Bad: direct string comparison leaks timing information
function verifyTokenInsecure(provided: string, expected: string): boolean {
return provided === expected;
}
Do not use passThroughOnException as error handling
passThroughOnException() is a fail-open mechanism that sends requests to your origin when your Worker throws an unhandled exception. While it can be useful during migration from an origin server, it hides bugs and makes debugging difficult. Use explicit try...catch blocks with structured error responses instead.
src/index.jsjs
// 🔴 Bad: hides errors by falling through to origin
const badHandler = {
async fetch(request, env, ctx) {
ctx.passThroughOnException();
const result = await handleRequest(request, env);
return Response.json(result);
},
};
// ✅ Good: explicit error handling with structured responses
export default {
async fetch(request, env, ctx) {
try {
const result = await handleRequest(request, env);
return Response.json(result);
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
console.error(
JSON.stringify({
message: "unhandled error",
error: message,
path: new URL(request.url).pathname,
}),
);
return Response.json({ error: "Internal server error" }, { status: 500 });
}
},
};
async function handleRequest(request, env) {
return { status: "ok" };
}
src/index.tsts
// 🔴 Bad: hides errors by falling through to origin
const badHandler = {
async fetch(
request: Request,
env: Env,
ctx: ExecutionContext,
): Promise<Response> {
ctx.passThroughOnException();
const result = await handleRequest(request, env);
return Response.json(result);
},
} satisfies ExportedHandler<Env>;
// ✅ Good: explicit error handling with structured responses
export default {
async fetch(
request: Request,
env: Env,
ctx: ExecutionContext,
): Promise<Response> {
try {
const result = await handleRequest(request, env);
return Response.json(result);
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
console.error(
JSON.stringify({
message: "unhandled error",
error: message,
path: new URL(request.url).pathname,
}),
);
return Response.json({ error: "Internal server error" }, { status: 500 });
}
},
} satisfies ExportedHandler<Env>;
async function handleRequest(request: Request, env: Env): Promise<object> {
return { status: "ok" };
}
Development and testing
Test with @cloudflare/vitest-plugin
The @cloudflare/vitest-plugin package runs your tests inside the Workers runtime, giving you access to real bindings (KV, R2, D1, Durable Objects) during tests. This catches issues that Node.js-based tests miss, like unsupported APIs or missing compatibility flags.
One known pitfall: the Vitest plugin automatically injects nodejs_compat, so tests pass even if your Wrangler configuration does not have the flag. Always confirm your wrangler.jsonc includes nodejs_compat if your code depends on Node.js built-in modules.
test/index.test.jsjs
import { describe, it, expect } from "vitest";
import { env } from "cloudflare:workers";
describe("KV operations", () => {
it("should store and retrieve a value", async () => {
await env.MY_KV.put("key", "value");
const result = await env.MY_KV.get("key");
expect(result).toBe("value");
});
it("should return null for missing keys", async () => {
const result = await env.MY_KV.get("nonexistent");
// ✅ Good: test the null case explicitly
expect(result).toBeNull();
});
});
test/index.test.tsts
import { describe, it, expect } from "vitest";
import { env } from "cloudflare:workers";
describe("KV operations", () => {
it("should store and retrieve a value", async () => {
await env.MY_KV.put("key", "value");
const result = await env.MY_KV.get("key");
expect(result).toBe("value");
});
it("should return null for missing keys", async () => {
const result = await env.MY_KV.get("nonexistent");
// ✅ Good: test the null case explicitly
expect(result).toBeNull();
});
});
For more information, refer to Testing with Vitest.
Related resources
- Rules of Durable Objects: best practices for stateful, coordinated applications.
- Rules of Workflows: best practices for durable, multi-step Workflows.
- Platform limits: CPU time, memory, subrequest, and other limits.
- Workers errors: error codes and debugging guidance.
Was this helpful?
YesNo
On this page
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/best-practices/workers-best-practices/#page","headline":"Workers Best Practices","description":"Code patterns and configuration guidance for building fast, reliable, observable, and secure Workers.","url":"https://developers.cloudflare.com/workers/best-practices/workers-best-practices/","inLanguage":"en","image":"https://developers.cloudflare.com/workers/best-practices/workers-best-practices/og.png?v=d435c8b7aebe8dd9","dateModified":"2026-10-12","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
description: Explore demo applications and reference architectures built with Cloudflare Workers. title: Demos and architectures image: https://developers.cloudflare.com/workers/demos/og.png?v=0cfc971b5d74a7ce
Documentation Index
Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt
Use this file to discover all available pages before exploring further.
Docs