This MySQL client error occurs when a prepared statement is executed without binding values to all the parameter placeholders (?) defined in the SQL query. The error indicates a mismatch between the expected parameters and the data actually provided.
The CR_PARAMS_NOT_BOUND (2031) error is a MySQL C API client error that signals an attempt to execute a prepared statement where one or more placeholder parameters have not been bound with data. In MySQL's prepared statement protocol, parameters must be explicitly bound before execution: 1. **Prepare**: Create a prepared statement with `mysql_stmt_prepare()` containing `?` placeholders 2. **Bind Parameters**: Assign values to each placeholder using `mysql_stmt_bind_param()` 3. **Execute**: Run the statement with `mysql_stmt_execute()` - at this point, MySQL verifies all parameters are bound 4. **Fetch Results**: Retrieve results if applicable 5. **Close**: Clean up with `mysql_stmt_close()` Error 2031 occurs when step 3 (execute) is attempted but step 2 (bind) was skipped, incomplete, or improperly done. The most common causes are forgetting to bind parameters, binding the wrong number of parameters, or using the wrong binding function. This error is prevalent in Node.js, Python, PHP, and other language bindings where prepared statement lifecycle is exposed but not always obvious. It's also a sign of logic errors where conditional binding or loop-based binding may skip parameters unexpectedly.
Ensure your SQL query has exactly as many ? placeholders as values you're binding:
#include <mysql.h>
#include <string.h>
MYSQL_STMT *stmt = mysql_stmt_init(mysql);
if (!stmt) {
fprintf(stderr, "mysql_stmt_init() failed\n");
return 1;
}
const char *query = "INSERT INTO users (name, email, age) VALUES (?, ?, ?)";
if (mysql_stmt_prepare(stmt, query, strlen(query))) {
fprintf(stderr, "Prepare failed: %s\n", mysql_stmt_error(stmt));
mysql_stmt_close(stmt);
return 1;
}
// Query has 3 placeholders - must bind exactly 3 parameters
MYSQL_BIND bind[3]; // Must match placeholder count!
memset(bind, 0, sizeof(bind));
// Bind parameter 1: name (string)
const char *name = "John Doe";
unsigned long name_length = strlen(name);
bind[0].buffer_type = MYSQL_TYPE_STRING;
bind[0].buffer = (void *)name;
bind[0].buffer_length = name_length;
bind[0].length = &name_length;
// Bind parameter 2: email (string)
const char *email = "[email protected]";
unsigned long email_length = strlen(email);
bind[1].buffer_type = MYSQL_TYPE_STRING;
bind[1].buffer = (void *)email;
bind[1].buffer_length = email_length;
bind[1].length = &email_length;
// Bind parameter 3: age (integer)
int age = 30;
bind[2].buffer_type = MYSQL_TYPE_LONG;
bind[2].buffer = (void *)&age;
// NOW bind all parameters before execute
if (mysql_stmt_bind_param(stmt, bind)) {
fprintf(stderr, "Bind failed: %s\n", mysql_stmt_error(stmt));
mysql_stmt_close(stmt);
return 1;
}
// ONLY NOW execute - all parameters are bound
if (mysql_stmt_execute(stmt)) {
fprintf(stderr, "Execute failed: %s\n", mysql_stmt_error(stmt));
mysql_stmt_close(stmt);
return 1;
}
mysql_stmt_close(stmt);Count the ? marks in your SQL - your MYSQL_BIND array must have exactly that many elements.
In Node.js with mysql or mysql2 drivers, ensure your parameter array matches placeholder count:
const mysql = require('mysql');
const connection = mysql.createConnection({
host: 'localhost',
user: 'root',
password: 'password',
database: 'mydb'
});
// WRONG: 3 placeholders but only 2 parameters
const query1 = 'INSERT INTO users (name, email, age) VALUES (?, ?, ?)';
const values1 = ['John', '[email protected]']; // Missing age!
// connection.query(query1, values1, callback); // ERROR 2031!
// CORRECT: Match placeholder count
const values2 = ['John', '[email protected]', 30]; // All 3 parameters
connection.query(query1, values2, (error, results) => {
if (error) throw error;
console.log('Insert successful');
});
// For SELECT with WHERE clause
const query3 = 'SELECT * FROM users WHERE id = ? AND active = ?';
const values3 = [123]; // WRONG: missing active parameter
const values4 = [123, 1]; // CORRECT: both parameters provided
connection.query(query3, values4, (error, results) => {
if (error) throw error;
console.log(results);
});
// Debugging: Log query and parameters
console.log('Query:', query3);
console.log('Parameters:', values4);
const placeholders = (query3.match(/\?/g) || []).length;
const provided = values4.length;
if (placeholders !== provided) {
console.error(`Mismatch: query has ${placeholders} placeholders but ${provided} parameters provided`);
}Always verify: placeholders_count === parameters_array.length
In PHP, ensure bind_param() is called before execute():
<?php
$mysqli = new mysqli("localhost", "user", "password", "database");
if ($mysqli->connect_error) {
die("Connection failed: " . $mysqli->connect_error);
}
// Prepare statement with 3 placeholders
$query = "INSERT INTO users (name, email, age) VALUES (?, ?, ?)";
$stmt = $mysqli->prepare($query);
if (!$stmt) {
die("Prepare failed: " . $mysqli->error);
}
// WRONG: Only binding 2 parameters for a query with 3 placeholders
// $stmt->bind_param("ss", $name, $email); // Missing age!
// $stmt->execute(); // ERROR 2031: missing age parameter
// CORRECT: Bind ALL parameters before execute
$name = "John Doe";
$email = "[email protected]";
$age = 30;
// bind_param signature: "types", var1, var2, var3, ...
// i = integer, d = double, s = string, b = blob
$stmt->bind_param("ssi", $name, $email, $age);
if (!$stmt->bind_param("ssi", $name, $email, $age)) {
die("Bind failed: " . $stmt->error);
}
if (!$stmt->execute()) {
die("Execute failed: " . $stmt->error);
}
echo "Insert successful";
$stmt->close();
$mysqli->close();
?>The type string in bind_param() must have one character per placeholder: "ssi" for 3 parameters (string, string, integer).
When parameters are conditionally added, ensure all ? placeholders are handled:
// WRONG: Building parameters conditionally can miss placeholders
let query = 'SELECT * FROM users WHERE 1=1';
let params = [];
if (filter.name) {
query += ' AND name = ?';
params.push(filter.name);
}
if (filter.email) {
query += ' AND email = ?';
params.push(filter.email);
}
if (filter.age) {
query += ' AND age = ?';
// Forgot to push filter.age!
}
// query might have 3 placeholders but only 2 parameters
db.query(query, params, callback); // May error if age filter is set
// CORRECT: Always push parameter if adding placeholder
let query2 = 'SELECT * FROM users WHERE 1=1';
let params2 = [];
if (filter.name) {
query2 += ' AND name = ?';
params2.push(filter.name);
}
if (filter.email) {
query2 += ' AND email = ?';
params2.push(filter.email);
}
if (filter.age) {
query2 += ' AND age = ?';
params2.push(filter.age); // ALWAYS push when adding ?
}
db.query(query2, params2, callback);
// Even better: Use a reusable helper
function buildQuery(filters) {
const conditions = [];
const params = [];
for (const [key, value] of Object.entries(filters)) {
if (value !== undefined && value !== null) {
conditions.push(`${key} = ?`);
params.push(value);
}
}
return {
query: `SELECT * FROM users WHERE ${conditions.join(' AND ')}`,
params: params
};
}
const { query: finalQuery, params: finalParams } = buildQuery(filter);
db.query(finalQuery, finalParams, callback);The golden rule: Every ? in the query must have a corresponding value in the parameters array.
Add logging to catch parameter mismatches early:
// JavaScript example
function executeQuery(sql, params) {
// Validation
const placeholderCount = (sql.match(/\?/g) || []).length;
const paramCount = Array.isArray(params) ? params.length : 0;
if (placeholderCount !== paramCount) {
console.error('ERROR: Parameter count mismatch!');
console.error(`SQL: ${sql}`);
console.error(`Expected ${placeholderCount} parameters, got ${paramCount}`);
console.error(`Parameters provided:`, params);
throw new Error(`CR_PARAMS_NOT_BOUND: ${placeholderCount} placeholders but ${paramCount} parameters`);
}
console.log(`Executing: ${sql}`);
console.log(`Parameters: ${JSON.stringify(params)}`);
return db.query(sql, params);
}
// Usage
try {
await executeQuery(
'INSERT INTO users (name, email) VALUES (?, ?)',
['John'] // Missing email - will throw with helpful message
);
} catch (err) {
console.error(err.message);
}This wrapper catches mismatches before they reach the database driver.
Bulk inserts can easily have parameter mismatches:
// WRONG: Building placeholders without matching parameters
const users = [
{ name: 'Alice', email: '[email protected]' },
{ name: 'Bob', email: '[email protected]' }
];
let query = 'INSERT INTO users (name, email) VALUES ';
let values = [];
for (let i = 0; i < users.length; i++) {
if (i > 0) query += ', ';
query += '(?, ?)';
// Forgot to push the actual values!
// values.push(users[i].name, users[i].email);
}
db.query(query, values, callback); // Query has 4 placeholders, values is empty!
// CORRECT: Synchronize placeholders with parameter push
let query2 = 'INSERT INTO users (name, email) VALUES ';
let values2 = [];
for (let i = 0; i < users.length; i++) {
if (i > 0) query2 += ', ';
query2 += '(?, ?)';
values2.push(users[i].name, users[i].email); // Push immediately
}
db.query(query2, values2, callback);
// Best practice: Use a helper function
function buildInsertQuery(table, records) {
if (records.length === 0) throw new Error('No records to insert');
const columns = Object.keys(records[0]);
const placeholders = columns.map(() => '?').join(', ');
const rowPlaceholders = Array(records.length)
.fill(null)
.map(() => `(${placeholders})`)
.join(', ');
const query = `INSERT INTO ${table} (${columns.join(', ')}) VALUES ${rowPlaceholders}`;
const params = records.flatMap(record => columns.map(col => record[col]));
return { query, params };
}
const { query: sql, params } = buildInsertQuery('users', users);
db.query(sql, params, callback);When building multi-row inserts, push parameter values immediately after adding the placeholder.
If reusing statement objects, ensure parameters are rebound:
// WRONG: Reusing statement without rebinding for new values
MYSQL_STMT *stmt = mysql_stmt_init(mysql);
mysql_stmt_prepare(stmt, "INSERT INTO logs (message) VALUES (?)", 45);
MYSQL_BIND bind[1];
memset(bind, 0, sizeof(bind));
const char *msg1 = "First log";
bind[0].buffer_type = MYSQL_TYPE_STRING;
bind[0].buffer = (void *)msg1;
bind[0].buffer_length = strlen(msg1);
mysql_stmt_bind_param(stmt, bind);
mysql_stmt_execute(stmt);
// Trying to execute again with different data
const char *msg2 = "Second log";
// WITHOUT rebinding, MySQL will still use msg1's data
mysql_stmt_execute(stmt); // Uses msg1 again, not msg2!
// CORRECT: Rebind before each execute with different data
mysql_stmt_bind_param(stmt, bind); // Rebind with msg1
mysql_stmt_execute(stmt);
bind[0].buffer = (void *)msg2; // Change buffer pointer
bind[0].buffer_length = strlen(msg2);
mysql_stmt_bind_param(stmt, bind); // Rebind with msg2
mysql_stmt_execute(stmt); // Now executes with msg2
mysql_stmt_close(stmt);Each time you change parameter values, you must call mysql_stmt_bind_param() again before execute().
The CR_PARAMS_NOT_BOUND error reveals several architectural considerations:
1. Language Abstraction Leaks: Higher-level languages (JavaScript, Python, PHP) abstract the MySQL prepared statement protocol, but the error can still surface if abstraction layers incorrectly handle parameter binding. ORMs sometimes skip binding parameters if they detect optimal paths, leading to this error.
2. Dynamic Query Construction: Building queries dynamically (WHERE clauses, bulk inserts) is error-prone because placeholders and parameters are constructed separately. Modern SQL builders (knex.js, SQLAlchemy) generate both in sync to prevent this. When rolling your own, treat placeholder addition and parameter addition as a single atomic operation.
3. Parameter Type Mismatches: Some drivers (especially ODBC connectors) may also throw this error if parameter data types don't match the SQL query expectation, even if counts match. Always verify both count AND type.
4. Connection State: The MYSQL_BIND array is tied to the statement handle and connection state. Some operations (like switching databases with USE) invalidate prepared statement state. Reusing a statement across connection pool switches may cause this error.
5. Debugging with Query Logs: Enable MySQL's general query log to see exact SQL: SET GLOBAL general_log = 1; SET GLOBAL log_output = 'TABLE';. Then check mysql.general_log table. However, prepared statements are logged differently than bound values, so you may see ? instead of actual values.
6. Performance Implication of Fixes: Adding parameter validation at every query layer adds small overhead. Use validation helpers only in development/logging, not in critical hot paths. Use TypeScript or static analysis in production code to catch these at compile time.
7. Migration from mysql to mysql2: The mysql2 driver in Promise mode handles some parameter binding differently. When upgrading legacy mysql code to mysql2, watch for subtle differences in how parameters are validated.
8. Batch Operations: For batch operations (1000+ rows), consider using LOAD DATA INFILE instead of parameterized INSERT ... VALUES (...), (...) statements. It bypasses parameter binding entirely and is orders of magnitude faster for bulk data.
EE_WRITE (3): Error writing file
How to fix "EE_WRITE (3): Error writing file" in MySQL
CR_DNS_SRV_LOOKUP_FAILED (2070): DNS SRV lookup failed
How to fix "CR_DNS_SRV_LOOKUP_FAILED (2070): DNS SRV lookup failed" in MySQL
ERROR 1146: Table 'database.table' doesn't exist
How to fix "ERROR 1146: Table doesn't exist" in MySQL
ERROR 1040: Too many connections
How to fix "ERROR 1040: Too many connections" in MySQL
EE_READ (2): Error reading file
How to fix "EE_READ (2): Error reading file" in MySQL