Error handling in BQL
BrowserQL doesn't abort a mutation when one step fails. This guide explains what happens to the rest of your query when a step errors or times out, what the HTTP response looks like, and how to control failure behavior.
- A Browserless API token from your account dashboard
- Familiarity with writing BQL mutations
A failed step doesn't abort the rest
Once a valid mutation reaches execution, every top-level field runs, in the order you wrote it, even if an earlier field fails. When a step errors, including a waitForSelector that times out, BQL records the error and moves on to the next field. (Requests that never reach execution, such as an invalid token or a malformed query, are covered under non-200 status below.)
mutation Example {
goto(url: "https://example.com") {
status
}
missing: waitForSelector(selector: "#never-appears", timeout: 2000) {
time
}
html {
html
}
}
Here missing times out, but html still runs and returns the page HTML. BQL runs each top-level field independently, so one field's failure never stops the fields after it.
What this means in practice:
- The failed field's entry in
dataisnull. Fields that succeed still return their real values. - Later steps run against whatever page state exists at that point. After a timed-out
goto, a followingscreenshotmay capture whatever the page reached rather than nothing, since agototimeout only bounds the wait for the load condition and doesn't guarantee the navigation committed. - A later step can still fail as a knock-on effect. If the element it needed never appeared it times out too, but it is attempted and reports its own error.
The HTTP status is 200 with an errors array
Any mutation that runs to completion returns HTTP 200, even when steps inside it failed. Per-step failures and timeouts are reported in a GraphQL errors array in the response body, alongside whatever data succeeded:
{
"data": {
"goto": { "status": 200 },
"missing": null,
"html": { "html": "<!doctype html>..." }
},
"errors": [
{
"message": "Timed out waiting for selector \"#never-appears\" (requestId: abc123)",
"path": ["missing"]
}
]
}
So a 200 doesn't mean everything worked. Check for an errors array in the body, not just the status code.
Validation errors, such as an unknown field or a bad argument, also come back as 200 with an errors array, with no execution at all.
When you get a non-200 status
A handful of conditions fail before or around execution and return a non-200 status:
| Status | Cause |
|---|---|
400 / 401 / 404 / 429 | Rejected before execution: malformed request, invalid token, unknown route, or concurrency and rate limits. |
408 | The whole request exceeded the overall session timeout (your timeout query parameter or plan limit). This is the session ending, not a single step timing out. |
500 | Execution threw outside GraphQL, such as an unparseable query. The body is still { "errors": [ ... ] }. |
Reading the error object
Errors follow the standard GraphQL shape. Each has a message, and step failures also carry a path whose value is the top-level field name or its alias, so you can tell which step failed. Browserless appends a request ID to every message:
Timed out waiting for selector "h1" (requestId: abc123)
Include that requestId when you contact support: it lets the team match your call to the server-side logs.
Controlling failure behavior
BrowserQL has no per-step "best attempt" or "continue on error" flag, because top-level steps are already non-fatal: the mutation continues regardless. If you're coming from the REST APIs, note that bestAttempt is a REST parameter and doesn't exist in BQL.
The two controls you do have:
- Per-step timeouts. Most mutations accept a
timeoutargument (milliseconds) to bound how long that step waits before it fails. Use it to stop a slow step from consuming the whole session. if/ifnotmutations. Branch on page state so a step only runs when it makes sense, for example onlysolvea CAPTCHA when its selector is present.
See Wait conditions for the full set of waitFor* mutations and their timeout arguments.
FAQ & Troubleshooting
Does a timed-out waitForSelector stop the rest of my mutation?
No. It records an error and the remaining fields still run. The waitForSelector field is null in data, and later fields return their own results (or their own errors). To make a step conditional instead of just letting it fail, wrap it in an if / ifnot mutation.
My mutation returned 200 but the data is wrong or missing
A 200 only means the request completed, not that every step succeeded. Look for an errors array in the response body: each entry's path names the field that failed. A field listed there will be null in data.
A step that uses an exported variable fails with a strange error
Chained variables (@export(as: "x") then ${x}) are substituted between steps as the mutation runs. If the exporting field failed, its value was never captured, so the ${x} placeholder in a later field is substituted with the text undefined (or stays as the literal ${x} if no earlier step has exported that name, such as a typo in the variable name). That usually surfaces as a confusing downstream error, like an invalid URL or selector containing the word "undefined", so check that the exporting step succeeded before relying on its variable. See Variables and the @export directive.