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

What usually fixes it

Go deeper

Documented occurrences

…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.