Skip to content

Commit 60631d5

Browse files
authored
Improve error messages and stack traces (#10632)
* Capture errors thrown in the default export of the entry Worker in order to preserve stack traces * Add errors playground * Add tests * Add changeset * Fix lock file
1 parent 783afeb commit 60631d5

File tree

18 files changed

+420
-68
lines changed

18 files changed

+420
-68
lines changed

.changeset/olive-rockets-find.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@cloudflare/vite-plugin": patch
3+
---
4+
5+
Ensure that correct error messages and stack traces are displayed.
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { describe, expect, test } from "vitest";
2+
import { isBuild, page, viteTestUrl } from "../../__test-utils__";
3+
4+
describe.runIf(!isBuild)(
5+
"error thrown in the default export of the entry Worker",
6+
async () => {
7+
test("displays the correct message", async () => {
8+
await page.goto(`${viteTestUrl}/default-export`);
9+
const errorOverlay = page.locator("vite-error-overlay");
10+
const message = await errorOverlay
11+
.locator(".message-body")
12+
.first()
13+
.textContent();
14+
expect(message).toMatch("a is not defined");
15+
});
16+
17+
test("displays the correct source link in the stack trace", async () => {
18+
await page.goto(`${viteTestUrl}/default-export`);
19+
const errorOverlay = page.locator("vite-error-overlay");
20+
const stack = errorOverlay.locator(".stack");
21+
const fileLink = await stack.locator(".file-link").first().textContent();
22+
expect(fileLink).toMatch(/\/errors\/src\/worker-a\/index.ts:24:17$/);
23+
});
24+
}
25+
);
26+
27+
describe.runIf(!isBuild)(
28+
"error thrown in a named entrypoint of the entry Worker",
29+
async () => {
30+
test("displays the correct message", async () => {
31+
await page.goto(`${viteTestUrl}/named-entrypoint`);
32+
const errorOverlay = page.locator("vite-error-overlay");
33+
const message = await errorOverlay
34+
.locator(".message-body")
35+
.first()
36+
.textContent();
37+
expect(message).toMatch("b is not defined");
38+
});
39+
40+
test("displays the correct source link in the stack trace", async () => {
41+
await page.goto(`${viteTestUrl}/named-entrypoint`);
42+
const errorOverlay = page.locator("vite-error-overlay");
43+
const stack = errorOverlay.locator(".stack");
44+
const fileLink = await stack.locator(".file-link").first().textContent();
45+
expect(fileLink).toMatch(/\/errors\/src\/worker-a\/index.ts:30:22$/);
46+
});
47+
}
48+
);
49+
50+
describe.runIf(!isBuild)(
51+
"error thrown in the default export of an auxiliary Worker",
52+
async () => {
53+
test("displays the correct message", async () => {
54+
await page.goto(`${viteTestUrl}/auxiliary-worker/default-export`);
55+
const errorOverlay = page.locator("vite-error-overlay");
56+
const message = await errorOverlay
57+
.locator(".message-body")
58+
.first()
59+
.textContent();
60+
expect(message).toMatch("c is not defined");
61+
});
62+
63+
test("displays the correct source link in the stack trace", async () => {
64+
await page.goto(`${viteTestUrl}/auxiliary-worker/default-export`);
65+
const errorOverlay = page.locator("vite-error-overlay");
66+
const stack = errorOverlay.locator(".stack");
67+
const fileLink = await stack.locator(".file-link").first().textContent();
68+
expect(fileLink).toMatch(/\/errors\/src\/worker-a\/index.ts:36:22$/);
69+
});
70+
}
71+
);
72+
73+
describe.runIf(!isBuild)(
74+
"error thrown in a named entrypoint of an auxiliary Worker",
75+
async () => {
76+
test("displays the correct message", async () => {
77+
await page.goto(`${viteTestUrl}/auxiliary-worker/named-entrypoint`);
78+
const errorOverlay = page.locator("vite-error-overlay");
79+
const message = await errorOverlay
80+
.locator(".message-body")
81+
.first()
82+
.textContent();
83+
expect(message).toMatch("d is not defined");
84+
});
85+
86+
test("displays the correct source link in the stack trace", async () => {
87+
await page.goto(`${viteTestUrl}/auxiliary-worker/named-entrypoint`);
88+
const errorOverlay = page.locator("vite-error-overlay");
89+
const stack = errorOverlay.locator(".stack");
90+
const fileLink = await stack.locator(".file-link").first().textContent();
91+
expect(fileLink).toMatch(/\/errors\/src\/worker-a\/index.ts:42:22$/);
92+
});
93+
}
94+
);
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"name": "@playground/errors",
3+
"private": true,
4+
"type": "module",
5+
"scripts": {
6+
"build": "vite build --app",
7+
"check:types": "tsc --build",
8+
"dev": "vite dev",
9+
"preview": "vite preview"
10+
},
11+
"devDependencies": {
12+
"@cloudflare/vite-plugin": "workspace:*",
13+
"@cloudflare/workers-tsconfig": "workspace:*",
14+
"@cloudflare/workers-types": "catalog:default",
15+
"typescript": "catalog:default",
16+
"vite": "catalog:vite-plugin",
17+
"wrangler": "workspace:*"
18+
}
19+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { WorkerEntrypoint } from "cloudflare:workers";
2+
3+
interface Env {
4+
NAMED_ENTRYPOINT: Service<NamedEntrypoint>;
5+
AUXILIARY_WORKER: Service;
6+
}
7+
8+
export class NamedEntrypoint extends WorkerEntrypoint {
9+
override fetch() {
10+
// @ts-expect-error: deliberate error
11+
console.log(b);
12+
13+
return new Response("Named entrypoint");
14+
}
15+
}
16+
17+
export default {
18+
async fetch(request, env) {
19+
const url = new URL(request.url);
20+
21+
switch (url.pathname) {
22+
case "/default-export": {
23+
// @ts-expect-error: deliberate error
24+
console.log(a);
25+
26+
return new Response("Default export");
27+
}
28+
case "/named-entrypoint": {
29+
// Note: if the response is returned directly then the stack trace is incorrect
30+
const response = await env.NAMED_ENTRYPOINT.fetch(request);
31+
32+
return response;
33+
}
34+
case "/auxiliary-worker/default-export": {
35+
// Note: if the response is returned directly then the stack trace is incorrect
36+
const response = await env.AUXILIARY_WORKER.fetch(request);
37+
38+
return response;
39+
}
40+
case "/auxiliary-worker/named-entrypoint": {
41+
// Note: if the response is returned directly then the stack trace is incorrect
42+
const response = await env.AUXILIARY_WORKER.fetch(request);
43+
44+
return response;
45+
}
46+
default: {
47+
return new Response("Fallback");
48+
}
49+
}
50+
},
51+
} satisfies ExportedHandler<Env>;
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"$schema": "../../node_modules/wrangler/config-schema.json",
3+
"name": "worker-a",
4+
"main": "./index.ts",
5+
"compatibility_date": "2024-12-30",
6+
"services": [
7+
{
8+
"binding": "NAMED_ENTRYPOINT",
9+
"service": "worker-a",
10+
"entrypoint": "NamedEntrypoint",
11+
},
12+
{
13+
"binding": "AUXILIARY_WORKER",
14+
"service": "worker-b",
15+
},
16+
],
17+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { WorkerEntrypoint } from "cloudflare:workers";
2+
3+
interface Env {
4+
NAMED_ENTRYPOINT: Service<NamedEntrypoint>;
5+
}
6+
7+
export class NamedEntrypoint extends WorkerEntrypoint {
8+
override fetch() {
9+
// @ts-expect-error: deliberate error
10+
console.log(d);
11+
12+
return new Response("Auxiliary Worker named entrypoint");
13+
}
14+
}
15+
16+
export default {
17+
async fetch(request, env) {
18+
const url = new URL(request.url);
19+
20+
switch (url.pathname) {
21+
case "/auxiliary-worker/default-export": {
22+
// @ts-expect-error: deliberate error
23+
console.log(c);
24+
25+
return new Response("Auxiliary Worker default export");
26+
}
27+
case "/auxiliary-worker/named-entrypoint": {
28+
return env.NAMED_ENTRYPOINT.fetch(request);
29+
}
30+
default: {
31+
return new Response("Auxiliary Worker fallback");
32+
}
33+
}
34+
},
35+
} satisfies ExportedHandler<Env>;
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"$schema": "../../node_modules/wrangler/config-schema.json",
3+
"name": "worker-b",
4+
"main": "./index.ts",
5+
"compatibility_date": "2024-12-30",
6+
"services": [
7+
{
8+
"binding": "NAMED_ENTRYPOINT",
9+
"service": "worker-b",
10+
"entrypoint": "NamedEntrypoint",
11+
},
12+
],
13+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"files": [],
3+
"references": [
4+
{ "path": "./tsconfig.node.json" },
5+
{ "path": "./tsconfig.worker.json" }
6+
]
7+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"extends": ["@cloudflare/workers-tsconfig/base.json"],
3+
"include": ["vite.config.ts", "__tests__"]
4+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"extends": ["@cloudflare/workers-tsconfig/worker.json"],
3+
"include": ["src"]
4+
}

0 commit comments

Comments
 (0)