"The experiment ran fine, but the data is not there" is one of the most common problems jsPsych users report. The frustrating part is that it has many unrelated causes, and the fix for one does nothing for another. This guide is organised by symptom: what you see, what is actually happening, how to confirm it, and what to change. Where Cognition handles a case for you, we say so, but most of the advice applies to any jsPsych setup.
Cause. jsPsych does not save anything by itself. It is a front-end library: it collects data in the participant's browser memory and, when the experiment ends, that memory is gone. jsPsych.data.displayData() only prints the data on screen; jsPsych.data.get().localSave() downloads a file to the participant's computer, not yours.
Confirm it. Look at your on_finish. If there is no request to a server (a fetch/POST, JATOS, Pavlovia, or similar), nothing is being stored anywhere.
Fix. You need a backend: your own server with a small PHP or Node script that receives jsPsych.data.get().json(), or a hosting platform that does it for you (JATOS, Pavlovia, Cognition). On Cognition you do not write any saving code: every trial is uploaded to the server as soon as it finishes, and the data appears in your task's run table.
Cause. A window.location = "..." placed in a trial's on_finish, in a jsPsychCallFunction trial, or inside a setTimeout unloads the page while data is still in flight. The browser cancels pending requests when it navigates, so the last block (or everything) never arrives.
Confirm it. Runs end abruptly at the same trial, usually the one right before the redirect. In the Network tab of the browser dev tools, the save request shows as cancelled.
Fix. Redirect only from the global on_finish of initJsPsych, and only after the save has completed:
const jsPsych = initJsPsych({
on_finish: () => {
window.location = "https://app.prolific.com/submissions/complete?cc=XXXXXX";
}
});
On Cognition this is safe as written: the platform uploads all pending trials, shows "Sending data to the experimenter… Please do not close this window", and only then runs your on_finish. Completion codes and redirects for Prolific, SONA and MTurk are covered in this dedicated article.
endExperiment() / abortExperiment() used incorrectlyCause. Ending early (failed attention check, wrong browser, declined a check) is legitimate, but how you do it matters. In jsPsych 6, calling jsPsych.endExperiment() did not wait for asynchronous saving to complete before the experiment-level on_finish ran, so a redirect placed there could fire while data was still uploading (see discussion #1850, which involved Cognition and Prolific). Support for Promise-returning on_finish callbacks was added in jsPsych 7 to fix this. Some researchers still copy that pattern, or simply stop the timeline by hiding the display element, which ends nothing.
Confirm it. Participants who should have been screened out have no data at all, not even the trials before the check.
Fix. In jsPsych 7 and 8 the end-early functions do trigger on_finish. Use jsPsych.abortExperiment("message") in v8 (jsPsych.endExperiment("message") in v7):
const attentionCheck = {
type: jsPsychHtmlButtonResponse,
stimulus: "<p>Please select the word <b>blue</b>.</p>",
choices: ["red", "blue", "green"],
data: { task: "attention_check" },
on_finish: (data) => {
data.correct = data.response === 1;
if (!data.correct) {
jsPsych.abortExperiment("<p>The session has ended. Thank you for your time.</p>");
}
}
};
If you are still on v6, the free v6/v7 to v8 migrator will update the syntax for you.
Cause. Nothing you can fully prevent. People get interrupted, get bored, or misclick.
Confirm it. On Cognition the run shows the status Dropped: the browser sends a pagehide beacon when the page is closed or navigated away before the end. While there are trials still pending upload, the participant also sees a beforeunload warning ("If you exit now the task will be lost"). The trials that were already uploaded are kept, and you can download them with the "not finished" filter in the download dialog.
Fix. Design for it: short blocks with breaks, a visible progress bar (initJsPsych({ show_progress_bar: true })), and put the measures you cannot afford to lose (demographics, the key manipulation check) early in the timeline rather than at the end.
Cause. Mobile participants, campus Wi-Fi, trains. A single failed request in a naive fetch at the end of the experiment means everything is lost.
Confirm it. Data gaps that correlate with mobile user agents, or runs that end mid-block with no JavaScript error.
Fix. Save incrementally and retry. Cognition sends pending trials roughly every 7 seconds and at the end; if a request fails it retries every second while the device is online, and otherwise waits for the browser's online event. A service worker also stores the data on the device and resends it in the background if the participant returns later. If they never come back, you still have every trial that was confirmed before the signal dropped, and the run ends up as Dropped or Timeout with partial data.
Cause. A plugin script you forgot to include, a misspelled stimulus path, an undefined value in a timeline variable. The experiment freezes on one screen, the participant waits, then leaves.
Confirm it. Open the browser console during a test run. On Cognition the run is flagged With errors, and the message and line number are stored with the run; a button asks the AI to explain the error and propose a fix. The editor's live preview shows the same console, so test there before publishing.
Fix. Preload media so missing files fail early and visibly:
const preload = {
type: jsPsychPreload,
images: stimuli.map(s => s.image),
on_error: (file) => console.error("Could not load", file)
};
timeline.unshift(preload);
Cause. The participant left the page open and walked away. On Cognition, a run that is still in progress more than one hour after it was created is marked Timeout. Its uploaded trials remain downloadable.
Fix. If your protocol genuinely needs more than an hour, split it into sessions with separate links rather than one very long run, and use trial_duration on trials where an answer is required so the timeline cannot stall forever.
Cause. The data was saved, but you never told jsPsych what to record. Typical slips: no data: {…} on the trial, so you cannot tell conditions apart; forgetting jsPsych.data.addProperties for participant-level variables; or computing accuracy in on_finish and writing it to a local variable instead of the data object.
Confirm it. Open one CSV and look for your own column names. If only rt, response and trial_type are present, this is it.
Fix.
jsPsych.data.addProperties({ condition: window.CONDITION });
const trial = {
type: jsPsychHtmlKeyboardResponse,
stimulus: jsPsych.timelineVariable("stimulus"),
choices: ["f", "j"],
trial_duration: 1500,
data: {
task: "response",
congruent: jsPsych.timelineVariable("congruent"),
correct_response: jsPsych.timelineVariable("correct_response")
},
on_finish: (data) => {
// write to data, not to a separate variable
data.correct = jsPsych.pluginAPI.compareKeys(data.response, data.correct_response);
}
};
On Cognition, URL query parameters such as PROLIFIC_PID are added as columns to every trial automatically, and each row also carries run_id and condition.
Cause. You opened the link yourself, or a link-preview fetched it, and you are now looking for a run that was never created. On Cognition, ?demo=true runs the task without creating a run or collecting data. Requests from bot and crawler user agents (including Facebook and WhatsApp link previews) do not create runs either. Conversely, if you share the link in a messaging app, be aware that the preview may "visit" it: the run table is not affected, but a self-hosted script with no such filter could record a ghost visit.
Fix. Test with a normal link and check the run table; use ?demo=true only when you do not want a run at all.
task, condition, correct) present and filled?on_finish of initJsPsych.jsPsych.abortExperiment() (v8) or jsPsych.endExperiment() (v7), never a hidden display or a hard redirect.jsPsychPreload trial loads every image, audio and video file, and the console shows no errors in the preview.trial_duration, and the whole session fits comfortably inside one hour.Most of these failures come down to one fact: the browser is not a storage device. A platform that uploads every trial as it happens, retries on bad connections, and labels each run as Finished, Dropped, Timeout or With errors turns "the data is gone" into "here is exactly where it stopped, and why". That is what Cognition does for jsPsych 6 to 8 tasks. Create a free account, paste your code, and run the checklist above on your own task.