Skip to content

Commit b2569be

Browse files
author
Vladimir Ubogovich
committed
Add stack trace for Airtable errors
Official client error class does not extend `Error` so no stack trace :( See Airtable/airtable.js#294
1 parent 3ec4b07 commit b2569be

File tree

6 files changed

+63
-24
lines changed

6 files changed

+63
-24
lines changed

.changeset/eighty-wombats-visit.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@qualifyze/airtable": major
3+
---
4+
5+
[BREAKING CHANGE] Introduce `AirtableError` extending `Error` to bring the stack trace very useful for debugging.
6+
The consumers should check their code for usage of `Airtable.Error` from the official client and replace it with the one from this library.

integration.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import Airtable from "airtable";
2-
import { AirtableRecord, Base, UnknownFields } from "./src";
2+
import { AirtableError, AirtableRecord, Base, UnknownFields } from "./src";
33

44
const apiKey = process.env.AIRTABLE_API_KEY;
55
const baseId = process.env.AIRTABLE_BASE_ID;
@@ -55,11 +55,12 @@ const validateRecord = (
5555
const validateNotFound = async <R>(target: () => Promise<R>) => {
5656
try {
5757
await target();
58-
throw new Error("Expected an error here");
5958
} catch (err: unknown) {
60-
if (err instanceof Airtable.Error && err.error === "NOT_FOUND") return;
59+
if (err instanceof AirtableError && err.error === "NOT_FOUND") return;
6160
throw err;
6261
}
62+
63+
throw new Error("Expected an error here");
6364
};
6465

6566
const main = async () => {
@@ -216,6 +217,6 @@ const main = async () => {
216217
};
217218

218219
main().catch((error) => {
219-
console.error(error);
220+
console.error(error.stack);
220221
process.exitCode = 1;
221222
});

src/error.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import type { Error as OfficialClientError } from "airtable";
2+
3+
// Use a custom error to bring the proper stack trace
4+
export class AirtableError extends Error {
5+
constructor(
6+
public error: string,
7+
message: string,
8+
public statusCode: number
9+
) {
10+
super(message);
11+
12+
if (Object.setPrototypeOf) {
13+
Object.setPrototypeOf(this, AirtableError.prototype);
14+
}
15+
}
16+
17+
static fromOfficialClientError({
18+
error,
19+
message,
20+
statusCode,
21+
}: OfficialClientError) {
22+
return new AirtableError(error, message, statusCode);
23+
}
24+
}

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@ export * from "./record";
44
export * from "./select-query";
55
export * from "./validator";
66
export * from "./official-client-wrapper";
7+
export * from "./error";
78
export * from "./fields";
89
export * from "./endpoint";

src/official-client-wrapper.ts

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Airtable from "airtable";
2+
import { AirtableError } from "./error";
23
import {
34
Endpoint,
45
EndpointOptions,
@@ -19,25 +20,32 @@ export class OfficialClientWrapper implements Endpoint {
1920
method: RestMethod,
2021
{ path, payload }: EndpointOptions<P>
2122
): Promise<unknown> {
22-
const { statusCode, headers, body } = await this.officialClient.makeRequest(
23-
{
24-
method,
25-
path: path === null ? undefined : `/${path}`,
26-
qs: payload?.query,
27-
body: payload?.body,
23+
try {
24+
const { statusCode, headers, body } =
25+
await this.officialClient.makeRequest({
26+
method,
27+
path: path === null ? undefined : `/${path}`,
28+
qs: payload?.query,
29+
body: payload?.body,
30+
});
31+
32+
if (!(+statusCode >= 200 && +statusCode < 300)) {
33+
throw new Error(
34+
`Airtable API responded with status code "${statusCode}, but no semantic error in response: ${JSON.stringify(
35+
{ headers, body },
36+
null,
37+
2
38+
)}`
39+
);
2840
}
29-
);
3041

31-
if (!(+statusCode >= 200 && +statusCode < 300)) {
32-
throw new Error(
33-
`Airtable API responded with status code "${statusCode}, but no semantic error in response: ${JSON.stringify(
34-
{ headers, body },
35-
null,
36-
2
37-
)}`
38-
);
42+
return body;
43+
} catch (err: unknown) {
44+
// Because official client error is not extended from Error so no stack trace
45+
if (err instanceof Airtable.Error) {
46+
throw AirtableError.fromOfficialClientError(err);
47+
}
48+
throw err;
3949
}
40-
41-
return body;
4250
}
4351
}

src/table.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import Airtable from "airtable";
2-
31
import { FieldsValidator, UnknownFields } from "./fields";
42
import { ActionPoint, ActionPointOptions } from "./action-point";
53
import { ValidationContext } from "./validator";
64
import { RestMethod, UnknownActionPayload } from "./endpoint";
5+
import { AirtableError } from "./error";
76
import { AirtableRecord } from "./record";
87
import { AirtableRecordDraft } from "./record-draft";
98
import { SelectQuery, SelectQueryParams } from "./select-query";
@@ -81,7 +80,7 @@ export class Table<Fields extends UnknownFields>
8180
// async/await are needed here to catch the error
8281
return await new AirtableRecordDraft(this, recordId).fetch();
8382
} catch (err: unknown) {
84-
if (err instanceof Airtable.Error && err.error === "NOT_FOUND") {
83+
if (err instanceof AirtableError && err.error === "NOT_FOUND") {
8584
return null;
8685
}
8786
throw err;

0 commit comments

Comments
 (0)