What is the expected pattern for handling query errors? I assume that when query.trigger()
resolves, the query has completed and errors would be populated then. But I'm getting unexpected results when I test it out.
I wrote a query that succeeds given condition: [1]
and fails given condition: []
. The query isn't important. My test code is:
async function test() {
await query1.trigger({additionalScope: {condition: [1]}})
console.log('query error:', query1.error)
}
...and between tests I changed the condition from [1]
to []
to trigger success/failure dynamically.
- First I ran it successfully first and
query1.error
was null.
- Then I ran it with
condition: []
to cause a failure. I expected the following log to indicate an error, but it indicated null.
- I added a
setTimeout
and then setInterval
around my console log to see if maybe query1.error
populated in a different execution stack, but query1.error
reported null forever (well, 30 seconds)
- Separately, I checked
query1.error
via the debug console and saw that it was populated with the error.
It seems like the actual query1
object is actually replaced with a different copy, any the copy available to the code that .trigger'd it is not the copy that reports errors. In my case, my goal is logic that does something like this:
await queryA.trigger();
if (queryA.error) {
throw Error(queryA.error)
}
// I don't want to run this if the first one failed, so I need to know right away.
// I don't *always* want to run queryB after queryA, so I don't want to use
// query chaining.
await queryB.trigger();
Am I doing something wrong?
Hey @Mark_Slade - thanks for reaching out. I think something pretty subtle is happening here, so you're not necessarily doing anything wrong.
By default, JS queries consider all global variable references to be static during its execution. You can override this behavior by toggling on the Keep variable references inside the query in sync with your app
option inside the query's Advanced
options menu.
I hope that helps! Let me know if you have any questions.
1 Like
That is subtle, indeed! I'm glad it was a quick fix. Thank you for getting me past it.
I would never have thought to look for a checkbox like this and find it pretty unintuitive. I have some suggestions that I think would make this subtlety harder to miss:
- The intellisense for SqlQueryUnified.error reads
If the query encountered an error while running, this field will be populated with the error message.
, but it seems like that's not entirely true (a new object with the error replaces the old object without the error, and your code needs an extra step to reference the new object).
- This page on error observability should probably mention it.
- This page in the docs on query#property-error could mention this.
- This is probably higher effort, and maybe not architecturally sound, but tucking the dynamic reference to variables under some new global variable/function with good intellisense would help me a lot. My first approach is to look at what globals are available to me, and I'm extremely appreciative that Retool's code/UI include intellisense support. If I saw something like
globalReferences
or getReference(variable)
with intellisense describing the nuance, I'd most likely stumble on it early. Further, my code would be more pure since it would no longer rely on an external setting to function correctly.
Thanks again,
Mark
Hey @Darren, I think I'm still doing something wrong. Here is everything I'm testing this behavior with:
query1 (my DB query):
SELECT t.id, tc.category_id
FROM track t
LEFT JOIN track_category tc ON t.id = tc.track_id
WHERE
category_id IN ({{ condition }})
;
testQueryError (my JS query that triggers it):
Configured with "Keep variable references etc...." checked.
console.log('Error before:', query1.error)
await query1.trigger({additionalScope: {condition: [1]}})
console.log('Error after:', query1.error)
Here are my results
Condition |
Previous run was... |
Logged state before |
After state |
Logged state after |
[] (invalid) |
Valid |
null |
"Some error..." |
null  |
[1] (valid) |
Invalid |
"Some error..." |
null |
null  |
[] (invalid) |
Invalid |
"Some error..." |
"Some error..." |
"Some error..."  |
So from this it appears that:
- If a query fails after previously succeeding, the error state after failing is not reported properly. <-- this is my blocker
- Weirdly, it works in the other direction. If a query succeeds after previously failing, the error state IS properly cleared.
- It also works out that if a query fails after previously failing, the error message remains correctly in-place.
Hmm interesting - I can definitely dig into this to see if it's actually something that just needs to be fixed. At first glance, though, it definitely looks like the error
attribute is being updated asynchronously in a way that makes it challenging to capture.
Would something like the below be a suitable replacement? It seems to provide the requisite conditional logic without locking you in to query chaining.
1 Like
I hadn't thought about that, but just tested and confirmed that it works.
Thanks for investigating the maybe-bug.
For discussion, what does the promise returned by trigger
represent? What I mean by that is I went in assuming that the promise would reject if the query failed, and was surprised that it resolves successfully. Then I thought it just represents successfully initiating the query (not completing it), but if that were true then the above would be a race condition.
I apologize for pressing for such a deep level of detail, but understanding how the platform works helps me build. I'm going with the above because it does seem to consistently work.
From what I understand, I think it is like this:
console.log(query1.data); // null
const result = await query1.trigger(); // Pretend it returned 5
console.log(result); // 5
console.log(query1.data); // 5
console.log(result === query1.data); // true
No need to apologize, @Mark_Slade - that's a perfectly valid question and not something that I've carefully considered before. Generally speaking, it looks like the trigger
promise resolves successfully as soon as its query returns anything. This is actually similar to how fetch
works, in the sense that its promise resolves successfully even with 4xx
or 5xx
level errors.