Question JS breaks after language switch (this.questionId undefined on re-run) | Experience Community
Skip to main content
Question

Question JS breaks after language switch (this.questionId undefined on re-run)

  • August 12, 2026
  • 3 replies
  • 53 views

vgayraud
QPN Level 8 ●●●●●●●●
Forum|alt.badge.img+65
  • QPN Level 8 ●●●●●●●●

New Survey Taking Experience. Switching language mid-survey remounts the question DOM and re-runs addOnReady. On that re-run this is a bare internal object:

[ctx] ee  hasQid= undefined  hasGetChoices= undefined  keys= []
getInstance(QID17)= function

So this.questionId is undefined, selectors built from it return null, and this.getChoices() throws. First pass is fine, only the translate replay is broken. Stack: onTranslate → onEntered → notify → forEach over registered custom JS.

Also: the outgoing tree is still mounted during the replay (duplicate section.question ids), and the JS source seems re-evaluated per render, so caching on the callback itself doesn't survive.

QuestionData.getInstance(qid) still works during the failing pass, so the workaround is to capture the qid on the first pass, park it outside the callback, and use getInstance() instead of this.

Scripts that only write embedded data are unaffected. Ones binding listeners or enforcing answer logic fail silently. Page looks fine, logic is gone.

What seems to have changed: these scripts ran fine for a long time, and the addOnReady re-run on language switch is longstanding behaviour I've relied on elsewhere. So the re-run itself isn't new, What looks new is the receiver: on the replay, this is no longer the QuestionData. Also seeing the outgoing and incoming trees briefly both mounted (duplicate section.question ids), though I can't say whether that part is new.

 

Anyone else seeing this, and does it line up with a JFE release on your side?

3 replies

vgayraud
QPN Level 8 ●●●●●●●●
Forum|alt.badge.img+65
  • Author
  • QPN Level 8 ●●●●●●●●
  • August 13, 2026

Follow-up: instrumented it. Two corrections to my post above.

It's a different class, not an unbound QuestionData:

initial mount : constructor=te  own keys=["questionId"]  39 prototype members
after switch : constructor=ee own keys=[] 11 prototype members

The 11 survivors are all page-level (clickNextButton, hideNextButton, getQuestionDisplayed…). Not one question-scoped method. The translate path passes the page-level receiver where the question-level one belongs.

Correction 1: getInstance() is only half usable. getQuestionContainer() works, but getChoices() / getAnswers() throw MissingQuestionRef.

Correction 2: Qualtrics.SurveyEngine.QuestionInfo survives fully populated, better fallback. QuestionInfo[qid].ChoicesgetChoices(), .AnswersgetAnswers(). Structure only, no response state.

All four hooks, and it persists for the life of the page:

switch → addOnUnload=ee → addOnload=ee → addOnReady=ee
then Next → addOnPageSubmit=ee

Control run, no switch: addOnPageSubmit gets te with a populated qid. And in one run addOnReady was ee at T+0 with addOnPageSubmit still ee 1000 ms later on a deliberate click, page settled, so not a transition window. Scope is per-page; the next page binds correctly again.

Reproduces on builds main.bcbf5a8d.js and main.1e789300.js, /jfe/ and /jfe2/, sandbox on sjc1 and pdx1, and client licenses on yul1.

Workaround that holds: capture this.questionId on the first pass into a window slot, read structure from QuestionInfo[qid], rely on this for nothing. The question JS source is re-evaluated per render, so caching on the callback function object does not survive.


Forum|alt.badge.img+8
  • QPN Level 4 ●●●●
  • August 21, 2026

Hi Vincent - Thanks for this post. We have been facing issues with our already deployed JavaScript code and this helped us understand the root cause. We are trying to develop a fix, but also submitted a ticket to Q Support. If you hear anything from Qualtrics on when/why this change was made and if it will be reverted, please let us know.


vgayraud
QPN Level 8 ●●●●●●●●
Forum|alt.badge.img+65
  • Author
  • QPN Level 8 ●●●●●●●●
  • August 22, 2026

Hi @jake_dufinetz

I submitted a ticket as well and asked that this be logged as a defect. Ticket's been escalated and is now in engineering's hands. Nothing back yet on why the change was made or whether it will be reverted. I'll post here as soon as I hear anything, and please do the same.

I started noticing parts of custom scripts failing as soon as June. this.questionId still resolved at that time but the accessors were already dropped. In August, the translate re-run receiver stopped carrying questionId as well.

Note that this possibly doesn't only impact custom scripts. I've noticed one bug linked to this in a Qualtrics native question: a location selector question with force response checked will always fail validation if you switch language at that question.

There are actually two separate problems here, which took a while to untangle.

1. The receiver. After a switch, hooks are re-invoked with a page-level object: no questionId, no question-level methods. Park the id on window on the first clean pass:

var SLOT = "unique_name";   // arbitrary, stable, unique per question in the page

if (!window.__qidSlots) { window.__qidSlots = {}; }
var qid = (this && this.questionId) || window.__qidSlots[SLOT];
if (!qid) { return; }
window.__qidSlots[SLOT] = qid;

window is required, not a local, not the function object. The source is re-evaluated per render, so anything cached on the callback itself starts empty.

Then never touch this again: qid for this.questionId, QuestionInfo[qid] for getChoices()/getAnswers(), the static Qualtrics.SurveyEngine.setJSEmbeddedData() for the instance call, a delegated listener instead of this.questionclick. Both DOM trees are mounted mid-transition, so use querySelectorAll('[id="question-' + qid + '"]') and loop.

2. The registry gets torn down, and this is the one that will bite you. After the switch, QuestionInfo and getInstance() stop working. They come back only if the respondent changes their answer afterwards (re-clicking the same choice fires a click but commits no state change, and the registry stayed dead.) On the normal path (answer, switch to check a wording, Next) they're gone for the rest of the page, including in addOnPageSubmit. Treat them as unavailable and don't design around the restoration case.

So don't read response state at submit. Capture as the respondent answers and write it out with the static call, so the submit hook reads nothing.

And if you fall back to the DOM, don't read input.checked. This cost me an afternoon. After a remount React restores the selection through the checked attribute, not the property, so a question the respondent answered before switching reads checked === false on every radio. My DOM fallback was dead exactly on the path it existed to cover. Across five paths, only the .selected class on the .choice wrapper was correct in every state:

var sel = container.querySelector('.choice.selected input[type="radio"]');
var m = (sel && sel.id) ? sel.id.match(/-(\d+)$/) : null;
if (m) { return m[1]; } // choice id off the input id suffix

That one isn't a Qualtrics bug, it's ordinary React behaviour. It only became reachable because the switch remounts the question mid-page, so any script written before in-survey translation existed has it latent.

Resolving instances. Never hold one across renders: a cached instance keeps returning correct getChoices() while getSelectedChoices() returns [] with no error. Resolve at call time and verify identity, because getInstance() returns the page-level object with questionId undefined without throwing:

function resolveInstance(qid) {
var inst;
try { inst = Qualtrics.SurveyEngine.QuestionData.getInstance(qid); }
catch (e) { return null; }
if (!inst || inst.questionId !== qid) { return null; }
return inst;
}

Treat both a throw and a failed identity check as indeterminate, never as "nothing selected."

All measured on main.cce32995.js, not documented behaviour, and it's already shifted once between June and August. Worth re-running on your own license.

Do post what Support comes back with, between two tickets (and maybe more) we might get a straighter answer.