Skip to content

Commit

Permalink
Add stack trace for Airtable errors
Browse files Browse the repository at this point in the history
Official client error class does not extend `Error` so no stack trace :(
See Airtable/airtable.js#294
  • Loading branch information
Vladimir Ubogovich committed Jan 27, 2022
1 parent 3ec4b07 commit 39cee68
Show file tree
Hide file tree
Showing 6 changed files with 59 additions and 24 deletions.
6 changes: 6 additions & 0 deletions .changeset/eighty-wombats-visit.md
@@ -0,0 +1,6 @@
---
"@qualifyze/airtable": major
---

[BREAKING CHANGE] Introduce `AirtableError` extending `Error` to bring the stack trace very useful for debugging.
The consumers should check their code for usage of `Airtable.Error` from the official client and replace it with the one from this library.
9 changes: 5 additions & 4 deletions integration.ts
@@ -1,5 +1,5 @@
import Airtable from "airtable";
import { AirtableRecord, Base, UnknownFields } from "./src";
import { AirtableError, AirtableRecord, Base, UnknownFields } from "./src";

const apiKey = process.env.AIRTABLE_API_KEY;
const baseId = process.env.AIRTABLE_BASE_ID;
Expand Down Expand Up @@ -55,11 +55,12 @@ const validateRecord = (
const validateNotFound = async <R>(target: () => Promise<R>) => {
try {
await target();
throw new Error("Expected an error here");
} catch (err: unknown) {
if (err instanceof Airtable.Error && err.error === "NOT_FOUND") return;
if (err instanceof AirtableError && err.error === "NOT_FOUND") return;
throw err;
}

throw new Error("Expected an error here");
};

const main = async () => {
Expand Down Expand Up @@ -216,6 +217,6 @@ const main = async () => {
};

main().catch((error) => {
console.error(error);
console.error(error.stack);
process.exitCode = 1;
});
20 changes: 20 additions & 0 deletions src/error.ts
@@ -0,0 +1,20 @@
import type { Error as OfficialClientError } from "airtable";

// Use a custom error to bring the proper stack trace
export class AirtableError extends Error {
constructor(
public error: string,
message: string,
public statusCode: number
) {
super(message);
}

static fromOfficialClientError({
error,
message,
statusCode,
}: OfficialClientError) {
return new AirtableError(error, message, statusCode);
}
}
1 change: 1 addition & 0 deletions src/index.ts
Expand Up @@ -4,5 +4,6 @@ export * from "./record";
export * from "./select-query";
export * from "./validator";
export * from "./official-client-wrapper";
export * from "./error";
export * from "./fields";
export * from "./endpoint";
42 changes: 25 additions & 17 deletions src/official-client-wrapper.ts
@@ -1,4 +1,5 @@
import Airtable from "airtable";
import { AirtableError } from "./error";
import {
Endpoint,
EndpointOptions,
Expand All @@ -19,25 +20,32 @@ export class OfficialClientWrapper implements Endpoint {
method: RestMethod,
{ path, payload }: EndpointOptions<P>
): Promise<unknown> {
const { statusCode, headers, body } = await this.officialClient.makeRequest(
{
method,
path: path === null ? undefined : `/${path}`,
qs: payload?.query,
body: payload?.body,
try {
const { statusCode, headers, body } =
await this.officialClient.makeRequest({
method,
path: path === null ? undefined : `/${path}`,
qs: payload?.query,
body: payload?.body,
});

if (!(+statusCode >= 200 && +statusCode < 300)) {
throw new Error(
`Airtable API responded with status code "${statusCode}, but no semantic error in response: ${JSON.stringify(
{ headers, body },
null,
2
)}`
);
}
);

if (!(+statusCode >= 200 && +statusCode < 300)) {
throw new Error(
`Airtable API responded with status code "${statusCode}, but no semantic error in response: ${JSON.stringify(
{ headers, body },
null,
2
)}`
);
return body;
} catch (err: unknown) {
// Because official client error is not extended from Error so no stack trace
if (err instanceof Airtable.Error) {
throw AirtableError.fromOfficialClientError(err);
}
throw err;
}

return body;
}
}
5 changes: 2 additions & 3 deletions src/table.ts
@@ -1,9 +1,8 @@
import Airtable from "airtable";

import { FieldsValidator, UnknownFields } from "./fields";
import { ActionPoint, ActionPointOptions } from "./action-point";
import { ValidationContext } from "./validator";
import { RestMethod, UnknownActionPayload } from "./endpoint";
import { AirtableError } from "./error";
import { AirtableRecord } from "./record";
import { AirtableRecordDraft } from "./record-draft";
import { SelectQuery, SelectQueryParams } from "./select-query";
Expand Down Expand Up @@ -81,7 +80,7 @@ export class Table<Fields extends UnknownFields>
// async/await are needed here to catch the error
return await new AirtableRecordDraft(this, recordId).fetch();
} catch (err: unknown) {
if (err instanceof Airtable.Error && err.error === "NOT_FOUND") {
if (err instanceof AirtableError && err.error === "NOT_FOUND") {
return null;
}
throw err;
Expand Down

0 comments on commit 39cee68

Please sign in to comment.