{"record":{"id":"ad9be17f8fbc2f79","repo":"GoogleChrome/lighthouse","slug":"save-already-in-progress","errorCode":null,"errorMessage":"Save already in progress","messagePattern":"Save already in progress","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"warning","filePath":"viewer/app/src/github-api.js","lineNumber":41,"sourceCode":"    this._saving = false;\n  }\n\n  static get LH_JSON_EXT() {\n    return '.lighthouse.report.json';\n  }\n\n  getFirebaseAuth() {\n    return this._auth;\n  }\n\n  /**\n   * Creates a gist under the users account.\n   * @param {LH.Result|LH.FlowResult} jsonFile The gist file body.\n   * @return {Promise<string>} id of the created gist.\n   */\n  async createGist(jsonFile) {\n    if (this._saving) {\n      throw new Error('Save already in progress');\n    }\n\n    logger.log('Saving report to GitHub...', false);\n    this._saving = true;\n\n    try {\n      const accessToken = await this._auth.getAccessToken();\n      let filename;\n      if ('steps' in jsonFile) {\n        filename = getFlowResultFilenamePrefix(jsonFile);\n      } else {\n        filename = getLhrFilenamePrefix({\n          finalDisplayedUrl: Util.getFinalDisplayedUrl(jsonFile),\n          fetchTime: jsonFile.fetchTime,\n        });\n      }\n      const body = {\n        description: 'Lighthouse json report',","sourceCodeStart":23,"sourceCodeEnd":59,"githubUrl":"https://github.com/GoogleChrome/lighthouse/blob/9515cd4e58ebed69f78742d932b501c2cab8ad8f/viewer/app/src/github-api.js#L23-L59","documentation":"Thrown by GithubApi.createGist to guard against overlapping saves: a class-level _saving flag is set true at the start of a gist creation and only reset in a finally block. A second createGist call while the first is still in flight is rejected outright. This is a re-entrancy guard, not a network error.","triggerScenarios":"Invoking createGist a second time before the first awaited call resolves — e.g. double-clicking the 'Save as gist' button, or two callers racing to save.","commonSituations":"Users double-clicking the save button; a UI that does not disable the control during the async save; programmatic retry triggered before the prior promise settles.","solutions":["Disable the save UI control while createGist is pending and re-enable it on settlement (success or error).","Await/queue the existing save instead of starting a new one — guard with a local promise or debounce.","Only call createGist when not already saving (check the in-flight promise at the call site)."],"exampleFix":"// before\nsaveBtn.onclick = () => githubApi.createGist(json); // double-click throws\n\n// after\nlet pending = null;\nsaveBtn.onclick = async () => {\n  if (pending) return pending;            // reuse in-flight save\n  saveBtn.disabled = true;\n  pending = githubApi.createGist(json).finally(() => {\n    pending = null;\n    saveBtn.disabled = false;\n  });\n  return pending;\n};","handlingStrategy":"validation","validationCode":"let pendingSave = null;\nasync function safeCreateGist(json) {\n  if (pendingSave) return pendingSave;\n  pendingSave = githubApi.createGist(json).finally(() => { pendingSave = null; });\n  return pendingSave;\n}","typeGuard":"// The _saving flag is internal; mirror it at the call site with an in-flight promise.\nconst isAlreadySaving = () => pendingSave !== null;","tryCatchPattern":"try {\n  await safeCreateGist(json);\n} catch (err) {\n  if (err.message === 'Save already in progress') {\n    // ignore — a save is already running; optionally await it\n  } else throw err;\n}","preventionTips":["Disable the save control during the in-flight createGist call.","Track an in-flight promise and reuse it instead of starting a parallel save.","Debounce button clicks on the save action."],"tags":["github-api","gist","concurrency","reentrancy"],"backgroundTag":null,"analyzedSha":"9515cd4e58ebed69f78742d932b501c2cab8ad8f","analyzedAt":"2026-08-13T06:28:10.346Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}