A lead researcher explaining the benefits of Cognition.run

Why Your jsPsych Data Is Not Saving (and How to Fix Each Cause)

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

1. "It works on my computer but nothing is saved"

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.

2. You redirect before the upload finishes

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.

3. endExperiment() / abortExperiment() used incorrectly

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

4. The participant closes the tab or presses Back

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.

5. Unstable connection or a phone that loses signal

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.

6. A JavaScript error at runtime

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);

7. Timeout

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.

8. The file is there but the important columns are empty

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.

9. Previews and bots

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.

Before you launch

  1. Run the full experiment once yourself and open the downloaded file: are your own columns (task, condition, correct) present and filled?
  2. Redirects and completion codes live only in the global on_finish of initJsPsych.
  3. Early exits use jsPsych.abortExperiment() (v8) or jsPsych.endExperiment() (v7), never a hidden display or a hard redirect.
  4. A jsPsychPreload trial loads every image, audio and video file, and the console shows no errors in the preview.
  5. Trials that require a response have a trial_duration, and the whole session fits comfortably inside one hour.
  6. Critical measures come early; blocks are short and a progress bar is on.
  7. Test once on a phone with mobile data, then check the run table for Dropped or With errors.
  8. Confirm that your test runs show up as Finished before you send the link to participants.

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.