Query BigQuery from TypeScript on Node, with the errors handled as documented
A small, correct client: install the official library, authenticate from the ambient identity with no key, run a parameterised query, and handle exactly the error reasons the vendor lists as retryable — with the three that must never be retried called out.
markdownguidev1DCMH-3D1Kstage: buildtrust: officialfamily: dataservice: bigqueryinterface: sdk@google-cloud/bigqueryparameterised queryambient credentialsApplication Default Credentialsretryable errorsexponential backoffroles/bigquery.jobUserroles/bigquery.dataViewer
Query BigQuery from TypeScript
Step 1 — the official client
npm i @google-cloud/bigquery
Check the installed package's own type definitions for every option you use. Google Cloud client libraries
rename and move options between major versions, and the .d.ts in node_modules is the only authority for the
version you actually have. This hub does not reproduce the API surface for that reason.
Step 2 — no key, ever
Construct the client with no credentials argument. It picks up the ambient identity: the attached service account on Cloud Run, Compute Engine, GKE with Workload Identity Federation, or your impersonated credentials locally.
Service account keys are "a security risk if not managed correctly", and the attached-identity path exists so
that none is needed (reference/identity/predefined-roles-and-permissions.md §5).
Step 3 — parameterise the query
Pass values as query parameters, not as interpolated strings. Two reasons, and the second is the one people forget:
- string interpolation into SQL is an injection vector;
- a parameterised query is the same query text every time, which matters for caching and for reading your
own slot usage.
Step 4 — handle exactly the errors the vendor says to retry
| Retry, with exponential backoff | Never retry |
|---|---|
backendError | invalidQuery — the SQL is wrong |
internalError | billingTierLimitExceeded — the on-demand model refused a query of that size. Retrying changes nothing; reduce the scan, or move to capacity pricing |
jobBackendError | accessDenied — a permission, not a transient |
jobInternalError | resourcesExceeded — the query has a stage that will not distribute |
rateLimitExceeded |
A client that retries the right-hand column is the most common wasteful pattern in a BigQuery integration: it turns a clear failure into a slow one, and on an on-demand project it can turn a refusal into a bill.
Step 5 — the two grants, which are separate
| To | Role |
|---|---|
| run a query (create a job) | a job-running role such as roles/bigquery.jobUser |
| read the data it touches | a data-reading role such as roles/bigquery.dataViewer, on the dataset |
Job permission and data permission are different grants on different objects. An identity with only the
first gets accessDenied on the data; with only the second it cannot start the job. Verify the exact role
names against the current roles reference before writing them into IaC
(reference/identity/predefined-roles-and-permissions.md).
Step 6 — verify
| Check | Expected |
|---|---|
| run the query from the deployed workload | rows, with no key in the deployment |
| run it with a deliberately wrong column name | invalidQuery — and your client does not retry |
| remove the dataset-level read role | accessDenied — proving which grant carries the data permission |
For a migration
This is also the smallest honest benchmark for a warehouse migration: the same query, parameterised, run
against the source and against BigQuery, with bytes scanned recorded. That number — not a node count — is
the input a BigQuery cost estimate needs (guides/parity/data.md §2).