Add typed API error classes (ApiHttpError, ApiTimeoutError, ApiNetworkError)
This commit is contained in:
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user