feat!: add ApifyApiError subclasses grouped by HTTP status - #1041
Merged
Conversation
vdusek
force-pushed
the
feat/api-error-subclasses
branch
from
September 8, 2026 10:20
41d3a23 to
cb0e82d
Compare
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
force-pushed
the
feat/api-error-subclasses
branch
from
September 8, 2026 10:23
cb0e82d to
e86355f
Compare
Contributor
|
See more at https://github.com/apify/apify-client-js/actions/runs/34215403308#summary-102025862578 |
barjin
approved these changes
Sep 8, 2026
This was referenced Sep 8, 2026
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*
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The client now throws the
ApifyApiErrorsubclass matching the response's HTTP status code, so acatchblock can branch oninstanceofinstead of comparingstatusCodenumbers ortypestrings. The split follows the status code becausetypeis per-endpoint and has hundreds of values. Same approach and same class names as the Python client in apify/apify-client-python#737.InvalidRequestErrorUnauthorizedErrorForbiddenErrorNotFoundErrorConflictErrorRateLimitErrorServerErrorAny other status stays a plain
ApifyApiError, and every subclass extends it, so existinginstanceof ApifyApiErrorchecks keep working.HttpClientbuilds the error through a hiddenApifyApiError.fromResponse()factory. The constructor is unchanged.typeis now typed asLiteralUnion<ApifyApiErrorType, string>, withApifyApiErrorTypecoming from the spec'sErrorTypeenum. Editors autocompleteerr.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.namenow holds the subclass name, so a printed stack starts withNotFoundError: ...instead ofApifyApiError: ....catchNotFoundOrThrowchecksinstanceof NotFoundError, soget()-style methods swallow every 404 whatever itstype. Before, onlyrecord-not-found,record-or-token-not-foundand HEAD requests were swallowed. The Python client made the same change, and the v3 upgrading guide covers it.waitForFinish()andcall(), which read a swallowed 404 as "the run isn't visible yet". A 404 that used to throw right away now keeps them polling untilwaitSecsruns out, and they end up throwing a generic error instead of theApifyApiError. A token revoked mid-wait lands there.Closes #709
✍️ Drafted by Claude Code