diff --git a/src/utils/errors/http-error.ts b/src/utils/errors/http-error.ts
new file mode 100644
index 0000000..8627f8a
--- /dev/null
+++ b/src/utils/errors/http-error.ts
@@ -0,0 +1,86 @@
+import { HttpResponse } from "@/utils/net";
+import { SoftError } from "./soft-error";
+
+/**
+ * Represents an HTTP error.
+ */
+export class HttpError extends SoftError {
+    /**
+     * The HTTP Response object associated with this error.
+     */
+    private readonly _response: HttpResponse;
+
+    /**
+     * Creates a new {@link HttpError} instance.
+     *
+     * @param response - The HTTP Response object associated with this error.
+     * @param message - The error message.
+     * @param isSoft - Indicates whether the error is recoverable or not.
+     */
+    constructor(response: HttpResponse, message?: string, isSoft?: boolean) {
+        super(isSoft ?? isServerError(response), message);
+
+        this.name = "HttpError";
+        this._response = response;
+    }
+
+    /**
+     * Gets the HTTP Response object associated with this error.
+     */
+    get response(): HttpResponse {
+        return this._response;
+    }
+
+    /**
+     * Extracts error information from the given HTTP Response object
+     * and returns an {@link HttpError} instance.
+     *
+     * @param response - The HTTP Response object to extract the error information from.
+     * @param isSoft - Indicates whether the error is recoverable or not.
+     *
+     * @returns A `Promise` that resolves to an {@link HttpError} instance.
+     */
+    static async fromResponse(response: HttpResponse, isSoft?: boolean): Promise<HttpError> {
+        const cachedResponse = HttpResponse.cache(response);
+        const errorText = `${response.status} (${
+            await cachedResponse.text()
+                .then(x => x && !isHtmlDocument(x) ? `${response.statusText}, ${x}` : response.statusText)
+                .catch(() => response.statusText)
+        })`;
+
+        return new HttpError(cachedResponse, errorText, isSoft);
+    }
+}
+
+/**
+ * Determines if the given error is an {@link HttpError}.
+ *
+ * @param error - The error to be checked.
+ *
+ * @returns `true` if the provided error is an instance of HttpError; otherwise, `false`.
+ */
+export function isHttpError(error: unknown): error is HttpError {
+    return error instanceof HttpError;
+}
+
+/**
+ * Determines whether the given `HttpResponse` represents a server error.
+ *
+ * @param response - The `HttpResponse` to check.
+ *
+ * @returns `true` if the response is a server error; otherwise, `false`.
+ */
+function isServerError(response: HttpResponse): boolean {
+    return response && (response.status === 429 || response.status >= 500);
+}
+
+/**
+ * Determines if the given text is an HTML document.
+ *
+ * @param text - The string to check.
+ *
+ * @returns `true` if the provided string is an HTML document; otherwise, `false`.
+ */
+function isHtmlDocument(text: string): boolean {
+    return text.startsWith("<!DOCTYPE html");
+}
diff --git a/tests/unit/utils/errors.ts/http-error.spec.ts b/tests/unit/utils/errors.ts/http-error.spec.ts
new file mode 100644
index 0000000..fd4d7f1
--- /dev/null
+++ b/tests/unit/utils/errors.ts/http-error.spec.ts
@@ -0,0 +1,116 @@
+import { HttpResponse } from "@/utils/net/http-response";
+import { HttpError, isHttpError } from "@/utils/errors/http-error";
+
+describe("HttpError", () => {
+    describe("constructor", () => {
+        test("initializes with given response and message", () => {
+            const response = HttpResponse.text("Resource does not exist", { status: 404, statusText: "Not Found" });
+
+            const error = new HttpError(response, "An error occurred.", false);
+
+            expect(error).toBeInstanceOf(HttpError);
+            expect(error.name).toBe("HttpError")
+            expect(error.message).toBe("An error occurred.");
+            expect(error.isSoft).toBe(false);
+            expect(error.response).toBe(response);
+        });
+
+        test("initializes with isSoft set to true for server error", () => {
+            const response = HttpResponse.text("Somebody knows what happened?", { status: 500, statusText: "Internal Server Error" });
+
+            const error = new HttpError(response);
+
+            expect(error.isSoft).toBe(true);
+            expect(error.response).toBe(response);
+        });
+
+        test("initializes with isSoft set to false for client error", () => {
+            const response = HttpResponse.text("It's not my fault!", { status: 400, statusText: "Bad Request" });
+
+            const error = new HttpError(response);
+
+            expect(error.isSoft).toBe(false);
+            expect(error.response).toBe(response);
+        });
+    });
+
+    describe("fromResponse", () => {
+        test("creates HttpError from given response", async () => {
+            const response = HttpResponse.text("Resource does not exist", { status: 404, statusText: "Not Found" });
+
+            const error = await HttpError.fromResponse(response, false);
+
+            expect(error).toBeInstanceOf(HttpError);
+            expect(error.name).toBe("HttpError")
+        });
+
+        test("includes text content in the error message", async () => {
+            const response = HttpResponse.json({ error: "Resource does not exist" }, { status: 404, statusText: "Not Found" });
+
+            const error = await HttpError.fromResponse(response, false);
+
+            expect(error.message).toBe(`404 (Not Found, ${JSON.stringify({ error: "Resource does not exist" })})`);
+        });
+
+        test("does not include HTML content in the error message", async () => {
+            const htmlContent = "<!DOCTYPE html><html><head><title>Not Found</title></head><body>Page Not Found</body></html>";
+            const response = HttpResponse.text(htmlContent, { status: 404, statusText: "Not Found" });
+
+            const error = await HttpError.fromResponse(response);
+
+            expect(error.message).toBe("404 (Not Found)");
+        });
+
+        test("returns soft error for server error codes", async () => {
+            const response = HttpResponse.text("Somebody knows what happened?", { status: 500, statusText: "Internal Server Error" });
+
+            const error = await HttpError.fromResponse(response);
+
+            expect(error.isSoft).toBe(true);
+        });
+
+        test("returns soft error for Too Many Requests error code (429)", async () => {
+            const response = HttpResponse.text("", { status: 429, statusText: "Too Many Requests" });
+
+            const error = await HttpError.fromResponse(response);
+
+            expect(error.isSoft).toBe(true);
+        });
+
+        test("returns non-soft error for client error codes", async () => {
+            const response = HttpResponse.text("It's not my fault!", { status: 400, statusText: "Bad Request" });
+
+            const error = await HttpError.fromResponse(response);
+
+            expect(error.isSoft).toBe(false);
+        });
+
+        test("can still read the response contents after the error is created", async () => {
+            const response = HttpResponse.json({ error: "Resource does not exist" }, { status: 404, statusText: "Not Found" });
+
+            const error = await HttpError.fromResponse(response);
+            const responseJson = await error.response.json();
+
+            expect(error.message).toBe(`404 (Not Found, ${JSON.stringify({ error: "Resource does not exist" })})`);
+            expect(responseJson).toEqual({ error: "Resource does not exist" });
+        });
+    });
+});
+
+describe("isHttpError", () => {
+    test("returns true for HttpError", () => {
+        const response = HttpResponse.text("Resource does not exist", { status: 404, statusText: "Not Found" });
+
+        const error = new HttpError(response, "An error occurred.", false);
+
+        expect(isHttpError(error)).toBe(true);
+    });
+
+    test("returns false for non-HttpError errors", () => {
+        expect(isHttpError(new Error("An error occurred."))).toBe(false);
+    });
+
+    test("returns false for non-error values", () => {
+        expect(isHttpError("An error occurred.")).toBe(false);
+    });
+});