Skip to content

feat!: add ApifyApiError subclasses grouped by HTTP status - #1041

Merged
vdusek merged 2 commits into
v3from
feat/api-error-subclasses
Sep 8, 2026
Merged

feat!: add ApifyApiError subclasses grouped by HTTP status#1041
vdusek merged 2 commits into
v3from
feat/api-error-subclasses

Conversation

@vdusek

@vdusek vdusek commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

The client now throws the ApifyApiError subclass matching the response's HTTP status code, so a catch block can branch on instanceof instead of comparing statusCode numbers or type strings. The split follows the status code because type is per-endpoint and has hundreds of values. Same approach and same class names as the Python client in apify/apify-client-python#737.

Status Class
400 InvalidRequestError
401 UnauthorizedError
403 ForbiddenError
404 NotFoundError
409 ConflictError
429 RateLimitError
5xx ServerError

Any other status stays a plain ApifyApiError, and every subclass extends it, so existing instanceof ApifyApiError checks keep working. HttpClient builds the error through a hidden ApifyApiError.fromResponse() factory. The constructor is unchanged.

type is now typed as LiteralUnion<ApifyApiErrorType, string>, with ApifyApiErrorType coming from the spec's ErrorType enum. Editors autocomplete err.type === 'actor-memory-limit-exceeded', which is what the Slack thread asked for, and any string the API returns still type-checks.

Breaking changes

  • error.name now holds the subclass name, so a printed stack starts with NotFoundError: ... instead of ApifyApiError: ....
  • catchNotFoundOrThrow checks instanceof NotFoundError, so get()-style methods swallow every 404 whatever its type. Before, only record-not-found, record-or-token-not-found and HEAD requests were swallowed. The Python client made the same change, and the v3 upgrading guide covers it.
  • The same helper backs waitForFinish() and call(), which read a swallowed 404 as "the run isn't visible yet". A 404 that used to throw right away now keeps them polling until waitSecs runs out, and they end up throwing a generic error instead of the ApifyApiError. A token revoked mid-wait lands there.

Closes #709

✍️ Drafted by Claude Code

@vdusek vdusek added the t-tooling Issues with this label are in the ownership of the tooling team. label Sep 8, 2026
@vdusek vdusek self-assigned this Sep 8, 2026
@vdusek
vdusek force-pushed the feat/api-error-subclasses branch from 41d3a23 to cb0e82d Compare September 8, 2026 10:20
An API error response is thrown as the `ApifyApiError` subclass matching its HTTP status code: `InvalidRequestError` (400), `UnauthorizedError` (401), `ForbiddenError` (403), `NotFoundError` (404), `ConflictError` (409), `RateLimitError` (429) and `ServerError` (5xx). Other status codes still throw a plain `ApifyApiError`. The `type` field is typed with the `ApifyApiErrorType` union generated from the OpenAPI spec, so editors autocomplete the known values.

BREAKING CHANGE: `error.name`, and the first line of the printed stack, carry the subclass name instead of `ApifyApiError`. Methods that swallow a 404 response, such as `get()` and `delete()`, now swallow every 404 regardless of its `type`, where before only `record-not-found` and `record-or-token-not-found` were swallowed.

Closes #709
@vdusek
vdusek force-pushed the feat/api-error-subclasses branch from cb0e82d to e86355f Compare September 8, 2026 10:23
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

⚠️ There are broken links in the documentation.

See more at https://github.com/apify/apify-client-js/actions/runs/34215403308#summary-102025862578

@vdusek
vdusek requested a review from barjin September 8, 2026 12:22
@vdusek
vdusek marked this pull request as ready for review September 8, 2026 12:22
@vdusek
vdusek requested a review from szaganek as a code owner September 8, 2026 12:22

@barjin barjin left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm, thank you @vdusek !

@vdusek
vdusek merged commit 4e40887 into v3 Sep 8, 2026
8 checks passed
@vdusek
vdusek deleted the feat/api-error-subclasses branch September 8, 2026 12:59
vdusek added a commit that referenced this pull request Sep 10, 2026
A 404 collapsed into `undefined` everywhere, including where it can't be
pinned to one resource: `run.dataset().get()` couldn't tell a missing
run from a missing dataset. Those calls now throw.

- Chained clients without an ID (`run.dataset()`, `run.keyValueStore()`,
`run.requestQueue()`, `run.log()`, `build.log()`) throw from `get()`,
`delete()`, `log().get()` and `log().stream()`. A new
`catchNotFoundForResourceOrThrow(err, this.id)` keys on the ID, so
ID-addressed clients still resolve to `undefined`.
- Fixed sub-paths throw and drop `| undefined`: `getStatistics()`,
`monthlyUsage()`, `limits()`, `getLog()`, `getInput()`, `test()`.
- Unchanged: `getRecord()`, `getRequest()`, `recordExists()` and
`lastRun()`.
- `version()`, `build()` and `envVar()` reject an empty string, which
used to address the collection.
- `UserClient.get()` is now `Promise<User | undefined>`, matching what
it always returned.
- `getStreamedLog()` on a missing run warns and stops.

Merging `v3` also moved `LogClient.stream()` under the same rule. #1041
made `catchNotFoundOrThrow()` a plain `instanceof NotFoundError` check,
so a streamed 404 matches it and `client.log(id).stream()` resolves to
`undefined` the way `get()` does. #1043 raised that flip as an open
question; the unusable error body it reports is still open.

Mirrors apify/apify-client-python#755.

Closes #1030

*✍️ Drafted by Claude Code*
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

t-tooling Issues with this label are in the ownership of the tooling team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants