{"id":"04af2712f91371d7","repo":"knex/knex","slug":"incomplete-onconflict-clause-onconflict-must-b","errorCode":null,"errorMessage":"Incomplete onConflict clause. .onConflict() must be directly followed by either .merge() or .ignore()","messagePattern":"Incomplete onConflict clause\\. \\.onConflict\\(\\) must be directly followed by either \\.merge\\(\\) or \\.ignore\\(\\)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"lib/query/querybuilder.js","lineNumber":1787,"sourceCode":"  }\n\n  // Sets insert query to ignore conflicts\n  ignore() {\n    this.builder._single.onConflict = this._columns;\n    this.builder._single.ignore = true;\n    return this.builder;\n  }\n\n  // Sets insert query to update on conflict\n  merge(updates) {\n    this.builder._single.onConflict = this._columns;\n    this.builder._single.merge = { updates };\n    return this.builder;\n  }\n\n  // Prevent\n  then() {\n    throw new Error(\n      'Incomplete onConflict clause. .onConflict() must be directly followed by either .merge() or .ignore()'\n    );\n  }\n}\n\nmodule.exports = Builder;\n","sourceCodeStart":1769,"sourceCodeEnd":1794,"githubUrl":"https://github.com/knex/knex/blob/e25d54bcb707714a17f5a5744eba5c4246bb4d1d/lib/query/querybuilder.js#L1769-L1794","documentation":"onConflict() returns an OnConflictBuilder that must be terminated by .merge() or .ignore() to set the actual conflict resolution. If the builder is awaited/executed while still on the OnConflictBuilder (i.e. neither terminator was called), its then() throws. This prevents silently running an INSERT that ignores the conflict intent the developer expressed.","triggerScenarios":"Calling knex('t').insert(...).onConflict('col') and then awaiting/executing it without chaining .merge() or .ignore(). Also triggered by .toString()/.toSQL() on the incomplete OnConflictBuilder.","commonSituations":"Developer forgets to finish the clause, or a conditional branch that adds .merge()/.ignore() is skipped at runtime.","solutions":["Always terminate .onConflict() with .merge() or .ignore()","Ensure any conditional logic that adds onConflict also adds the terminator unconditionally"],"exampleFix":"// before\nawait knex('users').insert({id:1, n:'a'}).onConflict('id');\n\n// after\nawait knex('users').insert({id:1, n:'a'}).onConflict('id').merge();","handlingStrategy":"validation","validationCode":"function completeOnConflict(builder, columns, mode) {\n  const oc = builder.onConflict(columns);\n  return mode === 'ignore' ? oc.ignore() : oc.merge();\n}","typeGuard":"null","tryCatchPattern":"try {\n  await knex('t').insert(v).onConflict('id');\n} catch (e) {\n  if (/Incomplete onConflict/.test(e.message)) { /* add .merge()/.ignore() */ }\n  else throw e;\n}","preventionTips":["Always chain .merge() or .ignore() immediately after .onConflict()","Use a helper that returns a completed OnConflictBuilder so it can never be left dangling","Lint for .onConflict( not followed by .merge( or .ignore("],"tags":["query-builder","upsert","on-conflict","api-misuse"],"analyzedSha":"e25d54bcb707714a17f5a5744eba5c4246bb4d1d","analyzedAt":"2026-08-03T18:35:32.148Z","schemaVersion":2}