Repository: cloudflare/workers-sdk
Pull request #9268: wrangler containers: Delete should have better error handling
Author: login: gabivlj
Created: 2025-05-15T15:54:55Z | Merged: 2025-05-20T16:38:41Z

Fixes issue where `wrangler containers delete` didn't show a proper error message when the API failed.

---

- Tests
  - [ ] TODO (before merge)
  - [x] Tests included
  - [ ] Tests not necessary because:
- Wrangler / Vite E2E Tests CI Job required? (Use "e2e" label or ask maintainer to run separately)
  - [ ] I don't know
  - [ ] Required
  - [x] Not required because: No e2e for `containers delete`.
- Public documentation
  - [ ] TODO (before merge)
  - [ ] Cloudflare docs PR(s): <!--e.g. <https://github.com/cloudflare/cloudflare-docs/pull/>...-->
  - [x] Documentation not necessary because: fix.
- Wrangler V3 Backport
  - [ ] TODO (before merge)
  - [ ] Wrangler PR: <!--e.g. <https://github.com/cloudflare/workers-sdk/pull/>...-->
  - [x] Not necessary because: containers is a new feature.
<!--
Have you read our [Contributing guide](https://github.com/cloudflare/workers-sdk/blob/main/CONTRIBUTING.md)?
In particular, for non-trivial changes, please always engage on the issue or create a discussion or feature request issue first before writing your code.
-->



Diff:
diff --git a/.changeset/curvy-hairs-attend.md b/.changeset/curvy-hairs-attend.md
new file mode 100644
index 0000000000..bbd5754f4a
--- /dev/null
+++ b/.changeset/curvy-hairs-attend.md
@@ -0,0 +1,5 @@
+---
+"wrangler": patch
+---
+
+`wrangler containers delete` handles API errors correctly
diff --git a/packages/wrangler/src/__tests__/containers/delete.test.ts b/packages/wrangler/src/__tests__/containers/delete.test.ts
index a377e83267..be3c48fd38 100644
--- a/packages/wrangler/src/__tests__/containers/delete.test.ts
+++ b/packages/wrangler/src/__tests__/containers/delete.test.ts
@@ -2,12 +2,14 @@ import { http, HttpResponse } from "msw";
 import patchConsole from "patch-console";
 import { mockAccount, setWranglerConfig } from "../cloudchamber/utils";
 import { mockAccountId, mockApiToken } from "../helpers/mock-account-id";
-import { mockConsoleMethods } from "../helpers/mock-console";
+import { mockCLIOutput, mockConsoleMethods } from "../helpers/mock-console";
 import { useMockIsTTY } from "../helpers/mock-istty";
 import { msw } from "../helpers/msw";
 import { runWrangler } from "../helpers/run-wrangler";
 
 describe("containers delete", () => {
+	const stdCli = mockCLIOutput();
+
 	const std = mockConsoleMethods();
 	const { setIsTTY } = useMockIsTTY();
 
@@ -43,6 +45,98 @@ describe("containers delete", () => {
 		`);
 	});
 
+	async function testStatusCode(code: number) {
+		setWranglerConfig({});
+		msw.use(
+			http.delete(
+				"*/applications/:id",
+				async ({ request }) => {
+					expect(await request.text()).toEqual("");
+					return new HttpResponse(`{"error": "something happened"}`, {
+						status: code,
+					});
+				},
+				{ once: true }
+			)
+		);
+		await expect(runWrangler("containers delete 123")).rejects
+			.toMatchInlineSnapshot(`
+			[Error: There has been an error deleting the container.
+			something happened]
+		`);
+		expect(stdCli.stderr).toMatchInlineSnapshot(`""`);
+		expect(stdCli.stdout).toMatchInlineSnapshot(`
+			"├ Loading account
+			│
+			├ Loading account
+			│
+			╭ Delete your container
+			│
+			"
+		`);
+	}
+
+	it("should delete container with 400", () => testStatusCode(400));
+	it("should delete container with 404", () => testStatusCode(404));
+
+	it("should delete container with 500", async () => {
+		setWranglerConfig({});
+		msw.use(
+			http.delete(
+				"*/applications/:id",
+				async ({ request }) => {
+					expect(await request.text()).toEqual("");
+					return new HttpResponse(`{"error": "something happened"}`, {
+						status: 500,
+					});
+				},
+				{ once: true }
+			)
+		);
+		await expect(runWrangler("containers delete 123")).rejects
+			.toMatchInlineSnapshot(`
+			[Error: There has been an unknown error deleting the container.
+			"{/"error/": /"something happened/"}"]
+		`);
+		expect(stdCli.stderr).toMatchInlineSnapshot(`""`);
+		expect(stdCli.stdout).toMatchInlineSnapshot(`
+			"├ Loading account
+			│
+			├ Loading account
+			│
+			╭ Delete your container
+			│
+			"
+		`);
+	});
+
+	it("should delete container", async () => {
+		setWranglerConfig({});
+		msw.use(
+			http.delete(
+				"*/applications/:id",
+				async ({ request }) => {
+					expect(await request.text()).toEqual("");
+					return new HttpResponse("{}");
+				},
+				{ once: true }
+			)
+		);
+		await runWrangler("containers delete 123");
+		expect(stdCli.stderr).toMatchInlineSnapshot(`""`);
+		expect(stdCli.stdout).toMatchInlineSnapshot(`
+			"├ Loading account
+			│
+			├ Loading account
+			│
+			╭ Delete your container
+			│
+			╰ Your container has been deleted
+
+			"
+		`);
+	});
+
 	it("should delete container (json)", async () => {
 		setIsTTY(false);
 		setWranglerConfig({});
diff --git a/packages/wrangler/src/containers/containers.ts b/packages/wrangler/src/containers/containers.ts
index 2c5a60092e..1e3775b875 100644
--- a/packages/wrangler/src/containers/containers.ts
+++ b/packages/wrangler/src/containers/containers.ts
@@ -8,7 +8,7 @@ import {
 import { processArgument } from "@cloudflare/cli/args";
 import { dim, gray } from "@cloudflare/cli/colors";
 import { inputPrompt, spinner } from "@cloudflare/cli/interactive";
-import { ApplicationsService } from "../cloudchamber/client";
+import { ApiError, ApplicationsService } from "../cloudchamber/client";
 import { loadAccountSpinner } from "../cloudchamber/common";
 import { wrap } from "../cloudchamber/helpers/wrap";
 import { UserError } from "../errors";
@@ -38,33 +38,51 @@ export async function deleteCommand(
 			"You must provide an ID. Use 'wrangler containers list` to view your containers."
 		);
 	}
-	if (deleteArgs.json || !isInteractive()) {
+
+	if (deleteArgs.json) {
 		const container = await ApplicationsService.deleteApplication(
 			deleteArgs.ID
 		);
 		console.log(JSON.stringify(container, null, 4));
 		return;
 	}
+
 	startSection("Delete your container");
-	const yes = await inputPrompt({
-		question:
-			"Are you sure that you want to delete these containers? The associated DO container will lose access to the containers.",
-		type: "confirm",
-		label: "",
-	});
-	if (!yes) {
-		cancel("The operation has been cancelled");
-		return;
+	if (isInteractive()) {
+		const yes = await inputPrompt({
+			question:
+				"Are you sure that you want to delete these containers? The associated DO container will lose access to the containers.",
+			type: "confirm",
+			label: "",
+		});
+		if (!yes) {
+			cancel("The operation has been cancelled");
+			return;
+		}
 	}
 
 	const [, err] = await wrap(
 		ApplicationsService.deleteApplication(deleteArgs.ID)
 	);
 	if (err) {
-		throw new UserError(
-			`There has been an internal error deleting your containers.\n ${err.message}`
+		if (err instanceof ApiError) {
+			if (err.status === 400 || err.status === 404) {
+				const body = JSON.parse(err.body);
+				throw new UserError(
+					`There has been an error deleting the container.\n${body.error}`
+				);
+			}
+
+			throw new Error(
+				`There has been an unknown error deleting the container.\n${JSON.stringify(err.body)}`
+			);
+		}
+
+		throw new Error(
+			`There has been an internal error deleting your containers.\n${err.message}`
 		);
 	}
+
 	endSection("Your container has been deleted");
 }
 


Reviews:
user: login: gpanders
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-15T15:57:07Z
user: login: edmundhung
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-16T11:12:23Z
user: login: petebacondarwin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T11:13:25Z
user: login: petebacondarwin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T11:14:29Z
user: login: petebacondarwin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T11:14:45Z
user: login: edmundhung
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-20T16:25:00Z

Review comments:
diff_hunk: @@ -38,33 +38,51 @@ export async function deleteCommand(
 			"You must provide an ID. Use 'wrangler containers list` to view your containers."
 		);
 	}
-	if (deleteArgs.json || !isInteractive()) {
+
+	if (deleteArgs.json) {
 		const container = await ApplicationsService.deleteApplication(
 			deleteArgs.ID
 		);
 		console.log(JSON.stringify(container, null, 4));
 		return;
 	}
+
 	startSection("Delete your container");
-	const yes = await inputPrompt({
-		question:
-			"Are you sure that you want to delete these containers? The associated DO container will lose access to the containers.",
-		type: "confirm",
-		label: "",
-	});
-	if (!yes) {
-		cancel("The operation has been cancelled");
-		return;
+	if (isInteractive()) {
+		const yes = await inputPrompt({
+			question:
+				"Are you sure that you want to delete these containers? The associated DO container will lose access to the containers.",
+			type: "confirm",
+			label: "",
+		});
+		if (!yes) {
+			cancel("The operation has been cancelled");
+			return;
+		}
 	}
 
 	const [, err] = await wrap(
 		ApplicationsService.deleteApplication(deleteArgs.ID)
 	);
 	if (err) {
+		if (err instanceof ApiError) {
+			if (err.status === 400 || err.status === 404) {
+				const body = JSON.parse(err.body);
+				throw new UserError(
+					`There has been an error deleting the container.\n${body.error}`
+				);
+			}
+
+			throw new UserError(
+				`There has been an unknown error deleting the container.\n${JSON.stringify(err.body)}`
+			);
path: packages/wrangler/src/containers/containers.ts
user: login: edmundhung
body: This doesn't seems like a user error, how about making it a `FatalError`?
created_at: 2025-05-16T11:05:45Z
author_association: MEMBER
diff_hunk: @@ -38,33 +38,51 @@ export async function deleteCommand(
 			"You must provide an ID. Use 'wrangler containers list` to view your containers."
 		);
 	}
-	if (deleteArgs.json || !isInteractive()) {
+
+	if (deleteArgs.json) {
 		const container = await ApplicationsService.deleteApplication(
 			deleteArgs.ID
 		);
 		console.log(JSON.stringify(container, null, 4));
 		return;
 	}
+
 	startSection("Delete your container");
-	const yes = await inputPrompt({
-		question:
-			"Are you sure that you want to delete these containers? The associated DO container will lose access to the containers.",
-		type: "confirm",
-		label: "",
-	});
-	if (!yes) {
-		cancel("The operation has been cancelled");
-		return;
+	if (isInteractive()) {
+		const yes = await inputPrompt({
+			question:
+				"Are you sure that you want to delete these containers? The associated DO container will lose access to the containers.",
+			type: "confirm",
+			label: "",
+		});
+		if (!yes) {
+			cancel("The operation has been cancelled");
+			return;
+		}
 	}
 
 	const [, err] = await wrap(
 		ApplicationsService.deleteApplication(deleteArgs.ID)
 	);
 	if (err) {
+		if (err instanceof ApiError) {
+			if (err.status === 400 || err.status === 404) {
+				const body = JSON.parse(err.body);
+				throw new UserError(
+					`There has been an error deleting the container.\n${body.error}`
+				);
+			}
+
+			throw new UserError(
+				`There has been an unknown error deleting the container.\n${JSON.stringify(err.body)}`
+			);
+		}
+
 		throw new UserError(
-			`There has been an internal error deleting your containers.\n ${err.message}`
path: packages/wrangler/src/containers/containers.ts
user: login: edmundhung
body: Same question on this one 🤔 
created_at: 2025-05-16T11:06:47Z
author_association: MEMBER
diff_hunk: @@ -38,33 +38,51 @@ export async function deleteCommand(
 			"You must provide an ID. Use 'wrangler containers list` to view your containers."
 		);
 	}
-	if (deleteArgs.json || !isInteractive()) {
+
+	if (deleteArgs.json) {
 		const container = await ApplicationsService.deleteApplication(
 			deleteArgs.ID
 		);
 		console.log(JSON.stringify(container, null, 4));
 		return;
 	}
+
 	startSection("Delete your container");
-	const yes = await inputPrompt({
-		question:
-			"Are you sure that you want to delete these containers? The associated DO container will lose access to the containers.",
-		type: "confirm",
-		label: "",
-	});
-	if (!yes) {
-		cancel("The operation has been cancelled");
-		return;
+	if (isInteractive()) {
+		const yes = await inputPrompt({
+			question:
+				"Are you sure that you want to delete these containers? The associated DO container will lose access to the containers.",
+			type: "confirm",
+			label: "",
+		});
+		if (!yes) {
+			cancel("The operation has been cancelled");
+			return;
+		}
 	}
 
 	const [, err] = await wrap(
 		ApplicationsService.deleteApplication(deleteArgs.ID)
 	);
 	if (err) {
+		if (err instanceof ApiError) {
+			if (err.status === 400 || err.status === 404) {
+				const body = JSON.parse(err.body);
+				throw new UserError(
+					`There has been an error deleting the container.\n${body.error}`
+				);
+			}
+
+			throw new UserError(
+				`There has been an unknown error deleting the container.\n${JSON.stringify(err.body)}`
+			);
path: packages/wrangler/src/containers/containers.ts
user: login: petebacondarwin
body: FatalError is also a UserError, just with an additional exitCode.
created_at: 2025-05-16T11:13:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -38,33 +38,51 @@ export async function deleteCommand(
 			"You must provide an ID. Use 'wrangler containers list` to view your containers."
 		);
 	}
-	if (deleteArgs.json || !isInteractive()) {
+
+	if (deleteArgs.json) {
 		const container = await ApplicationsService.deleteApplication(
 			deleteArgs.ID
 		);
 		console.log(JSON.stringify(container, null, 4));
 		return;
 	}
+
 	startSection("Delete your container");
-	const yes = await inputPrompt({
-		question:
-			"Are you sure that you want to delete these containers? The associated DO container will lose access to the containers.",
-		type: "confirm",
-		label: "",
-	});
-	if (!yes) {
-		cancel("The operation has been cancelled");
-		return;
+	if (isInteractive()) {
+		const yes = await inputPrompt({
+			question:
+				"Are you sure that you want to delete these containers? The associated DO container will lose access to the containers.",
+			type: "confirm",
+			label: "",
+		});
+		if (!yes) {
+			cancel("The operation has been cancelled");
+			return;
+		}
 	}
 
 	const [, err] = await wrap(
 		ApplicationsService.deleteApplication(deleteArgs.ID)
 	);
 	if (err) {
+		if (err instanceof ApiError) {
+			if (err.status === 400 || err.status === 404) {
+				const body = JSON.parse(err.body);
+				throw new UserError(
+					`There has been an error deleting the container.\n${body.error}`
+				);
+			}
+
+			throw new UserError(
+				`There has been an unknown error deleting the container.\n${JSON.stringify(err.body)}`
+			);
path: packages/wrangler/src/containers/containers.ts
user: login: petebacondarwin
body: In this case I would just rethrow the APIError?
created_at: 2025-05-16T11:14:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -38,33 +38,51 @@ export async function deleteCommand(
 			"You must provide an ID. Use 'wrangler containers list` to view your containers."
 		);
 	}
-	if (deleteArgs.json || !isInteractive()) {
+
+	if (deleteArgs.json) {
 		const container = await ApplicationsService.deleteApplication(
 			deleteArgs.ID
 		);
 		console.log(JSON.stringify(container, null, 4));
 		return;
 	}
+
 	startSection("Delete your container");
-	const yes = await inputPrompt({
-		question:
-			"Are you sure that you want to delete these containers? The associated DO container will lose access to the containers.",
-		type: "confirm",
-		label: "",
-	});
-	if (!yes) {
-		cancel("The operation has been cancelled");
-		return;
+	if (isInteractive()) {
+		const yes = await inputPrompt({
+			question:
+				"Are you sure that you want to delete these containers? The associated DO container will lose access to the containers.",
+			type: "confirm",
+			label: "",
+		});
+		if (!yes) {
+			cancel("The operation has been cancelled");
+			return;
+		}
 	}
 
 	const [, err] = await wrap(
 		ApplicationsService.deleteApplication(deleteArgs.ID)
 	);
 	if (err) {
+		if (err instanceof ApiError) {
+			if (err.status === 400 || err.status === 404) {
+				const body = JSON.parse(err.body);
+				throw new UserError(
+					`There has been an error deleting the container.\n${body.error}`
+				);
+			}
+
+			throw new UserError(
+				`There has been an unknown error deleting the container.\n${JSON.stringify(err.body)}`
+			);
path: packages/wrangler/src/containers/containers.ts
user: login: petebacondarwin
body: Or just an `Error`
created_at: 2025-05-16T11:14:45Z
author_association: CONTRIBUTOR

PR comments:
user: login: changeset-bot[bot]
created_at: 2025-05-15T15:55:00Z
body: ###  🦋  Changeset detected

Latest commit: 2c3335124c60acf4e94dbfadd9d573485b40a758

**The changes in this PR will be included in the next version bump.**

<details><summary>This PR includes changesets to release 2 packages</summary>

  | Name                            | Type  |
| ------------------------------- | ----- |
| wrangler                        | Patch |
| @cloudflare/vitest-pool-workers | Patch |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/cloudflare/workers-sdk/new/gv/delete?filename=.changeset/new-roses-trade.md&value=---%0A%22wrangler%22%3A%20patch%0A---%0A%0Awrangler%20containers%3A%20Delete%20should%20have%20better%20error%20handling%0A)


author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-15T15:58:26Z
body: A Wrangler prerelease is available for testing. You can install this latest build in your project with:

```sh
npm install --save-dev https://prerelease-registry.devprod.cloudflare.dev/workers-sdk/runs/15118398712/npm-package-wrangler-9268
```

<details><summary>Prereleases for other packages:</summary>


`cloudflare-workers-bindings-extension`:
```sh
wget https://prerelease-registry.devprod.cloudflare.dev/workers-sdk/runs/15118398712/npm-package-cloudflare-workers-bindings-extension-9268 -O ./cloudflare-workers-bindings-extension.0.0.0-v98deef251.vsix && code --install-extension ./cloudflare-workers-bindings-extension.0.0.0-v98deef251.vsix
```



`create-cloudflare`:
```sh
npx https://prerelease-registry.devprod.cloudflare.dev/workers-sdk/runs/15118398712/npm-package-create-cloudflare-9268 --no-auto-update
```



`@cloudflare/kv-asset-handler`:
```sh
npm install https://prerelease-registry.devprod.cloudflare.dev/workers-sdk/runs/15118398712/npm-package-cloudflare-kv-asset-handler-9268
```



`miniflare`:
```sh
npm install https://prerelease-registry.devprod.cloudflare.dev/workers-sdk/runs/15118398712/npm-package-miniflare-9268
```



`@cloudflare/pages-shared`:
```sh
npm install https://prerelease-registry.devprod.cloudflare.dev/workers-sdk/runs/15118398712/npm-package-cloudflare-pages-shared-9268
```



`@cloudflare/unenv-preset`:
```sh
npm install https://prerelease-registry.devprod.cloudflare.dev/workers-sdk/runs/15118398712/npm-package-cloudflare-unenv-preset-9268
```



`@cloudflare/vite-plugin`:
```sh
npm install https://prerelease-registry.devprod.cloudflare.dev/workers-sdk/runs/15118398712/npm-package-cloudflare-vite-plugin-9268
```



`@cloudflare/vitest-pool-workers`:
```sh
npm install https://prerelease-registry.devprod.cloudflare.dev/workers-sdk/runs/15118398712/npm-package-cloudflare-vitest-pool-workers-9268
```



`@cloudflare/workers-editor-shared`:
```sh
npm install https://prerelease-registry.devprod.cloudflare.dev/workers-sdk/runs/15118398712/npm-package-cloudflare-workers-editor-shared-9268
```



`@cloudflare/workers-shared`:
```sh
npm install https://prerelease-registry.devprod.cloudflare.dev/workers-sdk/runs/15118398712/npm-package-cloudflare-workers-shared-9268
```



`@cloudflare/workflows-shared`:
```sh
npm install https://prerelease-registry.devprod.cloudflare.dev/workers-sdk/runs/15118398712/npm-package-cloudflare-workflows-shared-9268
```


Note that these links will no longer work once [the GitHub Actions artifact expires](https://docs.github.com/en/organizations/managing-organization-settings/configuring-the-retention-period-for-github-actions-artifacts-and-logs-in-your-organization).

</details>
<!-- Sticky Pull Request Commentprerelease -->
author_association: CONTRIBUTOR
user: login: penalosa
created_at: 2025-05-19T13:09:53Z
body: @gabivlj have you had a chance to look at the comments on this?
author_association: CONTRIBUTOR
user: login: gabivlj
created_at: 2025-05-19T16:25:48Z
body: > @gabivlj have you had a chance to look at the comments on this?

Hey, yeah I was working on other stuff on Friday, going to apply the feedback now :+1: , thanks for the reviews as always Wrangler team <3
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-19T16:35:28Z
body: Failed to automatically backport this PR's changes to Wrangler v3. Please manually create a PR targeting the `v3-maintenance` branch with your changes. Thank you for helping us keep Wrangler v3 supported!

Depending on your changes, running `git rebase --onto v3-maintenance main gv/delete` might be a good starting point.
<!-- Sticky Pull Request Commentv3-backport -->
author_association: CONTRIBUTOR