{"record":{"id":"e823b9306b17fe48","repo":"Budibase/budibase","slug":"failed-to-delete-plugin-errmsg","errorCode":null,"errorMessage":"Failed to delete plugin: ${errMsg}","messagePattern":"Failed to delete plugin: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/pro/src/sdk/plugins/index.ts","lineNumber":124,"sourceCode":"  }\n}\n\nexport async function deletePlugin(pluginId: string) {\n  const db = tenancy.getGlobalDB()\n  try {\n    const plugin: Plugin = await db.get(pluginId)\n    const bucketPath = objectStore.getPluginS3Dir(plugin.name)\n    await objectStore.deleteFolder(\n      objectStore.ObjectStoreBuckets.PLUGINS,\n      bucketPath\n    )\n\n    await db.remove(pluginId, plugin._rev!)\n    await events.plugin.deleted(plugin)\n    await quotas.removePlugin()\n  } catch (err: any) {\n    const errMsg = err?.message ? err?.message : err\n    throw new Error(`Failed to delete plugin: ${errMsg}`)\n  }\n}\n\nexport async function checkPluginQuotas() {\n  const db = tenancy.getGlobalDB()\n  try {\n    const allPlugins = await db.allDocs(dbCore.getPluginParams())\n    const pluginCount = allPlugins.rows.length\n    console.log(`Syncing plugin count: ${pluginCount}`)\n    await quotas.updatePluginCount(pluginCount)\n  } catch (err) {\n    logging.logAlert(\"Unable to retrieve plugins for quota check\", err)\n  }\n}\n","sourceCodeStart":106,"sourceCodeEnd":139,"githubUrl":"https://github.com/Budibase/budibase/blob/a81a902e9a8fe55b467d106765f6638f12e35c49/packages/pro/src/sdk/plugins/index.ts#L106-L139","documentation":"deletePlugin performs several steps — removing the plugin doc from the global DB, emitting a plugin.deleted event, and decrementing the plugin quota. Any failure inside the try block (DB conflict/409, missing revision, event service error, quota decrement failure) is caught and rethrown wrapped as 'Failed to delete plugin: <cause>'. It normalizes non-Error throwables to a string message.","triggerScenarios":"Calling the plugin delete API when the plugin doc's _rev is stale (someone modified the plugin concurrently), the global DB is unreachable, the events/quotas service call fails, or the plugin doc was already deleted.","commonSituations":"Two admins deleting the same plugin simultaneously; CouchDB replication conflicts on the tenant's global database; network blip to the backing DB during the remove; quota service misconfiguration causing removePlugin() to throw after the doc was already removed.","solutions":["Re-fetch the plugin to get the current _rev and retry the delete","Check the wrapped errMsg to identify whether the failure is in db.remove, events.plugin.deleted, or quotas.removePlugin","Verify the tenant's global database is reachable and healthy (CouchDB health)","Check for concurrent deletions / replication conflicts on the plugin doc and retry once conflicts settle"],"exampleFix":"// before: deleting with a stale plugin object\nawait deletePlugin(oldPlugin)\n// after: refresh the doc, then delete\nconst db = tenancy.getGlobalDB()\nconst fresh = await db.get(pluginId)\nawait deletePlugin({ ...fresh, pluginId })","handlingStrategy":"try-catch","validationCode":"const fresh = await db.get(pluginId) // fails fast if already deleted / rev unknown","typeGuard":null,"tryCatchPattern":"try {\n  await deletePlugin(plugin)\n} catch (err: any) {\n  if (String(err.message).startsWith(\"Failed to delete plugin:\")) {\n    // re-fetch current _rev and retry once\n    const current = await tenancy.getGlobalDB().get(pluginId)\n    await deletePlugin({ ...plugin, _rev: current._rev })\n  } else { throw err }\n}","preventionTips":["Always delete from freshly fetched plugin docs","Avoid concurrent deletes of the same plugin","Check CouchDB health before bulk plugin maintenance"],"tags":["plugins","couchdb","concurrency"],"backgroundTag":"document-conflict","analyzedSha":"a81a902e9a8fe55b467d106765f6638f12e35c49","analyzedAt":"2026-08-29T01:03:10.972Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}