Add typed API error classes (ApiHttpError, ApiTimeoutError, ApiNetworkError)

This commit is contained in:
2026-04-14 23:30:00 +03:00
parent 454fb0bdb9
commit 6ef9ce4ed7
2 changed files with 92 additions and 0 deletions
+54
View File
@@ -0,0 +1,54 @@
import { describe, expect, it } from "vitest";
import { ApiError, ApiHttpError, ApiTimeoutError, ApiNetworkError } from "./errors.js";
describe("ApiError", () => {
it("extends Error with a message", () => {
const err = new ApiError("something broke");
expect(err).toBeInstanceOf(Error);
expect(err).toBeInstanceOf(ApiError);
expect(err.message).toBe("something broke");
expect(err.name).toBe("ApiError");
});
});
describe("ApiHttpError", () => {
it("has status and optional body", () => {
const err = new ApiHttpError("Not Found", 404, { detail: "missing" });
expect(err).toBeInstanceOf(ApiError);
expect(err).toBeInstanceOf(ApiHttpError);
expect(err.status).toBe(404);
expect(err.body).toEqual({ detail: "missing" });
expect(err.name).toBe("ApiHttpError");
});
it("body defaults to undefined", () => {
const err = new ApiHttpError("Server Error", 500);
expect(err.status).toBe(500);
expect(err.body).toBeUndefined();
});
});
describe("ApiTimeoutError", () => {
it("has timeoutMs", () => {
const err = new ApiTimeoutError(5000);
expect(err).toBeInstanceOf(ApiError);
expect(err.timeoutMs).toBe(5000);
expect(err.message).toContain("5000");
expect(err.name).toBe("ApiTimeoutError");
});
});
describe("ApiNetworkError", () => {
it("wraps a cause", () => {
const cause = new Error("ECONNREFUSED");
const err = new ApiNetworkError(cause);
expect(err).toBeInstanceOf(ApiError);
expect(err.cause).toBe(cause);
expect(err.name).toBe("ApiNetworkError");
});
it("works without a cause", () => {
const err = new ApiNetworkError();
expect(err.cause).toBeUndefined();
});
});
+38
View File
@@ -0,0 +1,38 @@
export class ApiError extends Error {
constructor(message: string) {
super(message);
this.name = "ApiError";
}
}
export class ApiHttpError extends ApiError {
readonly status: number;
readonly body?: unknown;
constructor(message: string, status: number, body?: unknown) {
super(message);
this.name = "ApiHttpError";
this.status = status;
this.body = body;
}
}
export class ApiTimeoutError extends ApiError {
readonly timeoutMs: number;
constructor(timeoutMs: number) {
super(`Request timed out after ${timeoutMs}ms`);
this.name = "ApiTimeoutError";
this.timeoutMs = timeoutMs;
}
}
export class ApiNetworkError extends ApiError {
override readonly cause?: Error | undefined;
constructor(cause?: Error) {
super(cause ? `Network error: ${cause.message}` : "Network error");
this.name = "ApiNetworkError";
this.cause = cause;
}
}