ErrLookup › Background articles › QueryError 'Query could not be completed' in ToolJet plugins — what the catch-all wrapper is hiding
QueryError 'Query could not be completed' in ToolJet plugins — what the catch-all wrapper is hiding
QueryError is the catch-all error class ToolJet plugins throw as 'Query could not be completed', 'Query execution failed', 'Connection test failed', and OAuth refresh-token failures. Developers meet it whenever a datasource operation — a database query, an API call, a token exchange, or a connection test — fails, and because plugins re-wrap the underlying cause inside a generic try/catch, the surfaced message rarely names the true problem on its own.
Distilled from 193 documented records across 3 repositories.
Background
QueryError is an Error subclass used throughout ToolJet's plugin layer. Plugin methods that talk to an external service — run() for queries, testConnection() for datasource checks, refreshToken() for OAuth — wrap their work in a try/catch and re-throw whatever fails as `new QueryError(message, description, data)`. It is the single funnel through which plugin failures reach ToolJet, so almost every error a developer sees from a ToolJet datasource is a QueryError regardless of the real cause underneath, whether that is a dead refresh token, an unreachable host, or a typo'd operation name.
The cost of that funnel is lossy wrapping. Many catches do not check whether the caught value is already a QueryError, so a structured inner error gets re-wrapped into a new QueryError with a generic title and the original collapsed into a stringified description or an empty data object. Token-refresh paths mask an 'access_token not found in the response' QueryError behind 'could not connect to SharePoint' or 'Error while generating refresh access token'; query paths collapse 'Invalid operation', Couchbase, and Firestore detail into 'Query could not be completed'. Where the inner data survives, it lives in the description (often JSON.stringify of the original error) or in the data slot — and which slot holds what is not consistent across plugins.
The family also carries recurring implementation defects. Several status guards use `(statusCode >= 200 || statusCode < 300)`, an expression true for every integer, which makes the non-2xx branch unreachable dead code; the intended check was AND, so 4xx error responses slip past and surface later at an access_token or catch block with a misleading message. Other catches read `error.response.*` without null-checking, so a network error with no `.response` turns into a TypeError that becomes the reported error instead of the real transport failure. Two plugins pass the whole error object as the message argument rather than error.message, producing an '[object Object]' message, and catches that JSON.parse a response body crash on the HTML or plaintext error page a reverse proxy returns.
How the error surfaces to the caller varies by plugin. Some preserve structured data well: the Intercom plugin keeps statusCode, the structured errors array, and the got error code; Firestore keeps Firestore status codes (7 for PERMISSION_DENIED, 5 for NOT_FOUND); Xero keeps an errorDetails object with ErrorNumber, Type, and modelState. Others pass `{}` as data and rely entirely on the message string, discarding the HTTP status entirely. So a developer diagnosing a QueryError must first learn which slot this plugin uses for its cause, then read there rather than trusting the title.
Common causes
- Expired or revoked OAuth refresh token.The most frequent trigger across the family. Google, Microsoft, and other token endpoints return an error body (invalid_grant, invalid_client) with no access_token field. Because of dead status guards this often surfaces as 'access_token not found in the response' or a masked refresh failure rather than naming invalid_grant; read data.responseObject.responseBody or the JSON-stringified description for the real error code. The fix is re-authorization with access_type=offline and prompt=consent, since a dead refresh token cannot be recovered.
- Upstream HTTP API error (4xx validation, 429 rate limit, 5xx outage).The external service returned a non-2xx response: 401/403 auth or scope failure, 404 wrong path or ID, 422 schema validation, 429 rate limit, or 5xx. Well-behaved plugins (Intercom, Xero) preserve statusCode and a structured error array in data; lossy ones collapse it into the generic message. Inspect data.statusCode first to classify the failure as auth, not-found, validation, or transient.
- Unrecognized operation enum value.queryOptions.operation is undefined, null, a typo, or a value not in the plugin's Operation enum (for example 'chat' instead of Operation.Chat, or 'bulk_upsert' instead of 'bulk_upsert_pkey'). The default switch branch throws 'Unsupported operation' or 'Invalid operation', which is then re-wrapped into 'Query could not be completed'. Re-select the operation from the dropdown so the canonical enum key is persisted, and keep client and server plugin versions in sync.
- Network or transport failure (DNS, TLS, connection refused, timeout).The fetch or got call failed before any HTTP exchange: host unreachable, wrong port, DNS resolution failure, TLS certificate rejection, or a proxy dropping the connection. A non-2xx HTTP response does NOT raise this — only fetch-layer failures do. These errors often lack a `.response`, which is exactly what trips the unguarded-catch defect and turns them into a TypeError.
- Malformed JSON in a query payload.A JSON.parse on a condition, key, options, or properties field threw a SyntaxError inside the try block, which the catch then normalized into 'Query could not be completed'. The bad JSON is the real cause; marshal these payloads with JSON.stringify rather than hand-writing JSON, and validate each field parses before calling run().
- Missing IAM permissions or access denied.The principal lacks the action the operation needs: dynamodb:Query on the table, bedrock:ListFoundationModels, or roles/datastore.user on the Firestore project. The upstream returns AccessDeniedException (AWS) or status 7 PERMISSION_DENIED (Firestore), which surfaces in errorDetails.code or the SDK message even though the title stays generic.
- Re-wrapping that destroys the original cause.Not a trigger itself but the reason the family is hard to diagnose: a catch that does not re-throw an existing QueryError flattens the structured inner error into a generic title with an empty or stringified data slot. This is why 'Query could not be completed' and 'could not connect to SharePoint' name neither the operation nor the OAuth code underneath.
What usually fixes it
- Re-throw QueryError instances unchanged: add `if (err instanceof QueryError) throw err;` at the top of every catch so the structured inner cause survives instead of being re-wrapped into a generic title.
- Read the description and data slots for the real cause, never the title. Look in data.responseObject.responseBody for the OAuth error envelope, data.statusCode and data.errors for API failures, errorDetails.code for Firestore, and the JSON-stringified description where the original error was flattened.
- Guard error.response before reading .statusMessage, .statusCode, or .body, and always pass error.message (a string) as the QueryError message argument — never the whole error object, which renders as '[object Object]'.
- Fix status-guard logic bugs: use `(statusCode >= 200 && statusCode < 300)` (AND, not OR) so the non-2xx branch is reachable and returns the real status and body instead of falling through to a misleading access_token check.
- Wrap JSON.parse on response bodies defensively — check Content-Type or try/catch and fall back to the raw string — so an HTML error page from a reverse proxy does not crash the catch and replace the real error with a SyntaxError.
- Validate inputs before calling run(): confirm operation is a known Operation enum value and that every JSON field (key, conditions, properties, table_parameters) parses, so a bad payload is rejected with a precise message rather than collapsed into the generic wrapper.
Go deeper
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
- Connection failures: ECONNREFUSED, ECONNRESET, and friends — why connections get refused, reset, or dropped.
- DNS resolution errors: ENOTFOUND and getaddrinfo failures — how hostname lookups fail and how to debug them.
- HTTP status errors: handling 4xx and 5xx responses — how to handle 4xx and 5xx responses properly.
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Documented occurrences
- Could not connect to Googles Calendar(ToolJet/ToolJet)
- could not connect to Google Calendar(ToolJet/ToolJet)
- access_token not found in the response(ToolJet/ToolJet)
- Connection failed(ToolJet/ToolJet)
- Query could not be completed(ToolJet/ToolJet)
- Unsupported Operation(ToolJet/ToolJet)
- Query could not be completed(ToolJet/ToolJet)
- access_token not found in the response(ToolJet/ToolJet)
- could not connect to Oauth server. status code(ToolJet/ToolJet)
- Query could not be completed(ToolJet/ToolJet)
- Refresh token failed(ToolJet/ToolJet)
- Query execution failed(ToolJet/ToolJet)
- Query could not be completed(ToolJet/ToolJet)
- Query could not be completed(ToolJet/ToolJet)
- Query execution failed(ToolJet/ToolJet)
- Connection test failed(ToolJet/ToolJet)
- Query could not be completed(ToolJet/ToolJet)
- Error while generating refresh access token(ToolJet/ToolJet)
- Query could not be completed(ToolJet/ToolJet)
- Query could not be completed(ToolJet/ToolJet)
…and 173 more across the corpus — use search.
Honest provenance: generated on 2026-08-13 from AI-assisted analysis of the linked records. See how records are made.