Skip to content

Commit 292c134

Browse files
authored
fix(smithy-client): instanceof support for ServiceException class (#1490)
* fix(smithy-client): instanceof support for ServiceException class * chore(changeset): changeset * chore(smithy-client): formatting fix * fix(smithy-client): update in line with the assertion table * chore(smithy-client): formatting fix * chore(smithy-client): explicit fallback to prototype check if a name doesn't exist
1 parent 7b03def commit 292c134

File tree

3 files changed

+133
-23
lines changed

3 files changed

+133
-23
lines changed

.changeset/yellow-hats-live.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@smithy/smithy-client": minor
3+
---
4+
5+
adds support for instanceof operator for ServiceException class

packages/smithy-client/src/exceptions.spec.ts

Lines changed: 103 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -34,39 +34,120 @@ it("ExceptionOptionType allows specifying message", () => {
3434
expect(exception.code).toBe("code");
3535
});
3636

37-
describe("ServiceException type checking", () => {
38-
const error = new ServiceException({
39-
name: "Error",
40-
$fault: "client",
41-
$metadata: {},
37+
describe("Exception Hierarchy Tests", () => {
38+
// test classes to represent the hierarchy
39+
class ClientServiceException extends ServiceException {
40+
constructor() {
41+
super({
42+
name: "ClientServiceException",
43+
$fault: "client",
44+
$metadata: {},
45+
});
46+
Object.setPrototypeOf(this, ClientServiceException.prototype);
47+
}
48+
}
49+
50+
class ModeledClientServiceException extends ClientServiceException {
51+
constructor() {
52+
super();
53+
this.name = "ModeledClientServiceException";
54+
Object.setPrototypeOf(this, ModeledClientServiceException.prototype);
55+
}
56+
}
57+
58+
describe("Empty Object Tests", () => {
59+
it("empty object should not be instanceof any exception", () => {
60+
expect({} instanceof Error).toBe(false);
61+
expect({} instanceof ServiceException).toBe(false);
62+
expect({} instanceof ClientServiceException).toBe(false);
63+
expect({} instanceof ModeledClientServiceException).toBe(false);
64+
});
4265
});
4366

44-
const duckTyped = {
45-
$fault: "server",
46-
$metadata: {},
47-
};
67+
describe("Error Instance Tests", () => {
68+
const error = new Error();
69+
it("Error instance should only be instanceof Error", () => {
70+
expect(error instanceof Error).toBe(true);
71+
expect(error instanceof ServiceException).toBe(false);
72+
expect(error instanceof ClientServiceException).toBe(false);
73+
expect(error instanceof ModeledClientServiceException).toBe(false);
74+
});
75+
});
76+
77+
describe("ServiceException Instance Tests", () => {
78+
const serviceException = new ServiceException({
79+
name: "ServiceException",
80+
$fault: "client",
81+
$metadata: {},
82+
});
83+
84+
it("ServiceException instance should be instanceof Error and ServiceException", () => {
85+
expect(serviceException instanceof Error).toBe(true);
86+
expect(serviceException instanceof ServiceException).toBe(true);
87+
expect(serviceException instanceof ClientServiceException).toBe(false);
88+
expect(serviceException instanceof ModeledClientServiceException).toBe(false);
89+
});
90+
});
91+
92+
describe("ClientServiceException Instance Tests", () => {
93+
const clientException = new ClientServiceException();
94+
it("ClientServiceException instance should be instanceof Error, ServiceException, and ClientServiceException", () => {
95+
expect(clientException instanceof Error).toBe(true);
96+
expect(clientException instanceof ServiceException).toBe(true);
97+
expect(clientException instanceof ClientServiceException).toBe(true);
98+
expect(clientException instanceof ModeledClientServiceException).toBe(false);
99+
});
100+
});
101+
102+
describe("ModeledClientServiceException Instance Tests", () => {
103+
const modeledException = new ModeledClientServiceException();
104+
it("ModeledClientServiceException instance should be instanceof Error, ServiceException, ClientServiceException, and ModeledClientServiceException", () => {
105+
expect(modeledException instanceof Error).toBe(true);
106+
expect(modeledException instanceof ServiceException).toBe(true);
107+
expect(modeledException instanceof ClientServiceException).toBe(true);
108+
expect(modeledException instanceof ModeledClientServiceException).toBe(true);
109+
});
110+
});
48111

49-
describe("isInstance", () => {
50-
it("should return true for ServiceException instances", () => {
51-
expect(ServiceException.isInstance(error)).toBe(true);
112+
describe("Duck-Typed Object Tests", () => {
113+
it("object with only name should not be instanceof any exception", () => {
114+
const obj = { name: "Error" };
115+
expect(obj instanceof Error).toBe(false);
116+
expect(obj instanceof ServiceException).toBe(false);
117+
expect(obj instanceof ClientServiceException).toBe(false);
118+
expect(obj instanceof ModeledClientServiceException).toBe(false);
52119
});
53120

54-
it("should return true for duck-typed objects", () => {
55-
expect(ServiceException.isInstance(duckTyped)).toBe(true);
121+
it("object with only $-props should be instanceof ServiceException", () => {
122+
const obj = { $fault: "client" as const, $metadata: {} };
123+
expect(obj instanceof Error).toBe(false);
124+
expect(obj instanceof ServiceException).toBe(true);
125+
expect(obj instanceof ClientServiceException).toBe(false);
126+
expect(obj instanceof ModeledClientServiceException).toBe(false);
56127
});
57128

58-
it("should return false for null or undefined", () => {
59-
expect(ServiceException.isInstance(null)).toBe(false);
60-
expect(ServiceException.isInstance(undefined)).toBe(false);
129+
it("object with ServiceException name and $-props should be instanceof ServiceException only", () => {
130+
const obj = { name: "ServiceException", $fault: "client" as const, $metadata: {} };
131+
expect(obj instanceof Error).toBe(false);
132+
expect(obj instanceof ServiceException).toBe(true);
133+
expect(obj instanceof ClientServiceException).toBe(false);
134+
expect(obj instanceof ModeledClientServiceException).toBe(false);
61135
});
62136

63-
it("should return false for invalid $fault values", () => {
64-
expect(ServiceException.isInstance({ $fault: "invalid", $metadata: {} })).toBe(false);
137+
it("object with ClientServiceException name and $-props should be instanceof ServiceException and ClientServiceException", () => {
138+
const obj = { name: "ClientServiceException", $fault: "client" as const, $metadata: {} };
139+
expect(obj instanceof Error).toBe(false);
140+
expect(obj instanceof ServiceException).toBe(true);
141+
expect(obj instanceof ClientServiceException).toBe(true);
142+
expect(obj instanceof ModeledClientServiceException).toBe(false);
65143
});
66144

67-
it("should return false for missing properties", () => {
68-
expect(ServiceException.isInstance({ $fault: "client" })).toBe(false);
69-
expect(ServiceException.isInstance({ $metadata: {} })).toBe(false);
145+
it("object with ModeledClientServiceException name and $-props should be instanceof ServiceException and ModeledClientServiceException", () => {
146+
const obj = { name: "ModeledClientServiceException", $fault: "client" as const, $metadata: {} };
147+
expect(obj instanceof Error).toBe(false);
148+
expect(obj instanceof ServiceException).toBe(true);
149+
expect(obj instanceof ClientServiceException).toBe(false);
150+
expect(obj instanceof ModeledClientServiceException).toBe(true);
70151
});
71152
});
72153
});

packages/smithy-client/src/exceptions.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export class ServiceException extends Error implements SmithyException, Metadata
3232

3333
constructor(options: ServiceExceptionOptions) {
3434
super(options.message);
35-
Object.setPrototypeOf(this, ServiceException.prototype);
35+
Object.setPrototypeOf(this, Object.getPrototypeOf(this).constructor.prototype);
3636
this.name = options.name;
3737
this.$fault = options.$fault;
3838
this.$metadata = options.$metadata;
@@ -50,6 +50,30 @@ export class ServiceException extends Error implements SmithyException, Metadata
5050
(candidate.$fault === "client" || candidate.$fault === "server")
5151
);
5252
}
53+
54+
/**
55+
* Custom instanceof check to support the operator for ServiceException base class
56+
*/
57+
public static [Symbol.hasInstance](instance: unknown): boolean {
58+
// Handle null/undefined
59+
if (!instance) return false;
60+
const candidate = instance as ServiceException;
61+
// For ServiceException, check only $-props
62+
if (this === ServiceException) {
63+
return ServiceException.isInstance(instance);
64+
}
65+
// For subclasses, check both prototype chain and name match
66+
// Note: instance must be ServiceException first (having $-props)
67+
if (ServiceException.isInstance(instance)) {
68+
// Only do name comparison if both sides have non-empty names
69+
if (candidate.name && this.name) {
70+
return this.prototype.isPrototypeOf(instance) || candidate.name === this.name;
71+
}
72+
// Otherwise fall back to just prototype check
73+
return this.prototype.isPrototypeOf(instance);
74+
}
75+
return false;
76+
}
5377
}
5478

5579
/**

0 commit comments

Comments
 (0)