This MySQL error occurs when you attempt to execute a COMMIT statement or an operation that causes an implicit commit inside a stored function or trigger, which MySQL does not permit. The error prevents transaction control statements from executing in these contexts.
ERROR 1422 (HY000) is a MySQL restriction error that enforces a critical database design limitation: transaction control is not allowed within stored functions or triggers. Key aspects of this error: 1. **Transaction Control Restriction**: MySQL prohibits explicit COMMIT, ROLLBACK, and implicit commit-causing statements inside stored functions and triggers. This restriction maintains database consistency and prevents nested transaction issues. 2. **Explicit vs Implicit Commits**: An explicit COMMIT is obvious in the code, but implicit commits are caused by certain DDL statements (ALTER TABLE, CREATE TABLE, DROP TABLE, TRUNCATE, etc.) or transaction-control statements (BEGIN, LOCK TABLES, etc.) that automatically commit the current transaction. 3. **Stored Procedures vs Stored Functions**: Critically, stored procedures ARE allowed to contain COMMIT/ROLLBACK statements, but stored functions are NOT. If a stored procedure uses these statements, it cannot be called from within a function or trigger. 4. **Trigger Context**: Triggers execute as part of a parent statement's transaction. Allowing commits inside triggers would break transaction integrity and create cascading commit scenarios. 5. **Deadlock Scenario**: In rare cases, when InnoDB detects a deadlock, it may attempt rollback operations. If this occurs inside a trigger or function, MySQL raises error 1422 even without explicit commit statements in the code. This is a structural MySQL limitation designed to maintain ACID properties and prevent transaction nesting complications.
First, locate the exact statement causing the error:
-- Show the creation code of the stored function/trigger
SHOW CREATE FUNCTION function_name;
SHOW CREATE TRIGGER trigger_name;
-- Check for problematic statements in MySQL Workbench or command-line
-- Look for these keywords inside the function/trigger body:
-- - COMMIT
-- - ROLLBACK
-- - START TRANSACTION or BEGIN (if not part of BEGIN...END block)
-- - TRUNCATE
-- - DDL statements (ALTER, CREATE, DROP)
-- - Administrative statementsExample problematic code:
CREATE TRIGGER my_trigger AFTER INSERT ON users
FOR EACH ROW
BEGIN
INSERT INTO audit_log VALUES (NEW.id, NOW());
COMMIT; -- ERROR 1422: This is not allowed in triggers
END;CREATE FUNCTION check_user(user_id INT) RETURNS BOOLEAN
BEGIN
DECLARE count INT;
SELECT COUNT(*) INTO count FROM users WHERE id = user_id;
COMMIT; -- ERROR 1422: Functions cannot have COMMIT
RETURN count > 0;
END;Remove transaction control statements from stored functions and triggers:
-- WRONG: Function with COMMIT
CREATE FUNCTION get_user_count() RETURNS INT
BEGIN
DECLARE cnt INT;
SELECT COUNT(*) INTO cnt FROM users;
COMMIT; -- ERROR 1422
RETURN cnt;
END;
-- CORRECT: Remove the COMMIT statement
CREATE FUNCTION get_user_count() RETURNS INT
BEGIN
DECLARE cnt INT;
SELECT COUNT(*) INTO cnt FROM users;
RETURN cnt;
END;-- WRONG: Trigger with ROLLBACK
CREATE TRIGGER validate_user BEFORE INSERT ON users
FOR EACH ROW
BEGIN
IF NEW.email IS NULL THEN
ROLLBACK; -- ERROR 1422
END IF;
END;
-- CORRECT: Use SIGNAL to raise error instead
CREATE TRIGGER validate_user BEFORE INSERT ON users
FOR EACH ROW
BEGIN
IF NEW.email IS NULL THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'Email cannot be null';
END IF;
END;The key principle: remove COMMIT/ROLLBACK entirely from functions and triggers.
TRUNCATE causes an implicit commit, so replace it with DELETE:
-- WRONG: TRUNCATE in trigger (causes implicit commit)
CREATE TRIGGER reset_cache AFTER DELETE ON items
FOR EACH ROW
BEGIN
TRUNCATE TABLE cache_table; -- ERROR 1422
END;
-- CORRECT: Use DELETE instead
CREATE TRIGGER reset_cache AFTER DELETE ON items
FOR EACH ROW
BEGIN
DELETE FROM cache_table;
END;-- WRONG: TRUNCATE in function
CREATE FUNCTION clear_logs() RETURNS INT
BEGIN
TRUNCATE TABLE logs; -- ERROR 1422
RETURN 1;
END;
-- CORRECT: Use DELETE
CREATE FUNCTION clear_logs() RETURNS INT
BEGIN
DELETE FROM logs;
RETURN ROW_COUNT();
END;Note: DELETE may be slower on large tables but preserves trigger context. For bulk operations, handle outside the trigger.
DDL statements (ALTER TABLE, CREATE TABLE, DROP TABLE, etc.) cause implicit commits:
-- WRONG: ALTER TABLE inside function
CREATE FUNCTION add_column() RETURNS INT
BEGIN
ALTER TABLE users ADD COLUMN new_field INT; -- ERROR 1422
RETURN 1;
END;
-- CORRECT: Execute schema changes outside stored routines
-- Run DDL in application code, migrations, or manually:
ALTER TABLE users ADD COLUMN new_field INT;
-- Or use a stored procedure (not function) for complex operations
CREATE PROCEDURE schema_migration()
BEGIN
-- Can use transaction control, but cannot be called from triggers
ALTER TABLE users ADD COLUMN new_field INT;
COMMIT;
END;Best practice: Keep schema changes in application migrations or management scripts, not in functions/triggers.
If you need transaction control, use stored procedures (which allow COMMIT/ROLLBACK) instead of functions:
-- WRONG: Function with complex transaction logic
CREATE FUNCTION transfer_money(from_id INT, to_id INT, amount DECIMAL) RETURNS BOOLEAN
BEGIN
START TRANSACTION; -- ERROR 1422
UPDATE accounts SET balance = balance - amount WHERE id = from_id;
UPDATE accounts SET balance = balance + amount WHERE id = to_id;
COMMIT; -- ERROR 1422
RETURN TRUE;
END;
-- CORRECT: Use a stored procedure instead
CREATE PROCEDURE transfer_money(from_id INT, to_id INT, amount DECIMAL, OUT success BOOLEAN)
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
SET success = FALSE;
END;
START TRANSACTION;
UPDATE accounts SET balance = balance - amount WHERE id = from_id;
UPDATE accounts SET balance = balance + amount WHERE id = to_id;
COMMIT;
SET success = TRUE;
END;
-- Call the procedure from your application
CALL transfer_money(1, 2, 100.00, @success);
SELECT @success;Key differences:
- Functions: Read-only, cannot modify database structure, no transaction control
- Procedures: Can modify data, control transactions, better for complex operations
Functions are meant for computation, procedures for business logic.
In triggers and functions, use SIGNAL to raise errors instead of rollback:
-- WRONG: Trying to rollback in trigger
CREATE TRIGGER validate_salary BEFORE INSERT ON employees
FOR EACH ROW
BEGIN
IF NEW.salary < 0 THEN
ROLLBACK; -- ERROR 1422
END IF;
END;
-- CORRECT: Use SIGNAL to raise error
CREATE TRIGGER validate_salary BEFORE INSERT ON employees
FOR EACH ROW
BEGIN
IF NEW.salary < 0 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'Salary cannot be negative';
END IF;
END;-- Comprehensive error handling example
CREATE TRIGGER process_order BEFORE INSERT ON orders
FOR EACH ROW
BEGIN
-- Validate inventory
IF (SELECT quantity FROM inventory WHERE product_id = NEW.product_id) < NEW.qty THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'Insufficient inventory';
END IF;
-- Validate customer
IF NOT EXISTS (SELECT 1 FROM customers WHERE id = NEW.customer_id) THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'Customer not found';
END IF;
-- Update inventory (implicit part of trigger transaction)
UPDATE inventory SET quantity = quantity - NEW.qty
WHERE product_id = NEW.product_id;
END;SIGNAL raises an error that causes the parent statement to roll back naturally, without explicit ROLLBACK statements.
After modifying your stored objects, test them thoroughly:
-- Test function without transaction control
DELIMITER //
CREATE FUNCTION get_user_summary(user_id INT) RETURNS VARCHAR(500)
BEGIN
DECLARE summary VARCHAR(500);
SELECT CONCAT(first_name, ' ', last_name, ' - ', email)
INTO summary
FROM users
WHERE id = user_id;
RETURN COALESCE(summary, 'User not found');
END //
DELIMITER ;
-- Test the function
SELECT get_user_summary(1);
SELECT get_user_summary(999); -- Non-existent user-- Test trigger behavior
DELIMITER //
CREATE TRIGGER log_user_creation AFTER INSERT ON users
FOR EACH ROW
BEGIN
INSERT INTO audit_log (action, user_id, created_at)
VALUES ('CREATE_USER', NEW.id, NOW());
END //
DELIMITER ;
-- Test trigger by inserting data
INSERT INTO users (first_name, last_name, email)
VALUES ('John', 'Doe', '[email protected]');
-- Verify trigger executed
SELECT * FROM audit_log WHERE user_id = LAST_INSERT_ID();-- Verify error handling works
-- This INSERT should fail with trigger validation error
INSERT INTO employees (name, salary) VALUES ('Test', -1000);
-- Expected: Error 1644 - SIGNAL error from trigger validationVerify that:
- Functions return correct values
- Triggers execute without ERROR 1422
- Data modifications occur as expected
- Error signals are raised properly
Understanding MySQL transaction restrictions at a deeper level:
1. Transaction Context Isolation: Triggers execute as part of their parent statement's transaction. Allowing commits inside would break the atomic nature of the parent operation. For example, if an UPDATE statement with a trigger could commit partway through, it would violate ACID properties.
2. Nested Transactions Not Supported: MySQL does not support true nested transactions (unlike some other databases). Savepoints (SAVEPOINT/RELEASE SAVEPOINT) are allowed in triggers, but full transaction control is not.
3. Savepoint Exception: Interestingly, SAVEPOINT and ROLLBACK TO SAVEPOINT are allowed in triggers because they don't end the transaction - they only roll back to a specific point within it. This allows partial rollback without breaking the parent transaction.
4. Stored Procedure Calling Chain: The restriction is transitive: if a stored procedure uses COMMIT, and that procedure is called from a function (which is valid), it still cannot execute because the function context prevents it. The calling chain matters.
5. Implicit Commits in System Statements: User management statements (CREATE USER, GRANT, REVOKE) and administrative statements (FLUSH, RESET) cause implicit commits because they operate on system tables. These cannot be used in functions/triggers for the same reason.
6. Deadlock and Error Context: In concurrent scenarios, if InnoDB detects a deadlock within a trigger, it would internally attempt rollback. The error 1422 (confusing in this context) is MySQL's way of saying "you're in a context where transaction control is not allowed, so I can't handle this deadlock properly."
7. Alternative Patterns: For complex workflows:
- Event-Driven: Use triggers for simple operations; handle complex logic in application code
- Work Queue: Store pending operations in a table; process them via scheduled procedures or application jobs
- Outbox Pattern: Instead of multiple updates in a trigger, write to an "outbox" table and process asynchronously
8. Performance Implications: Avoiding transaction control in triggers means simpler, faster execution. Complex transaction logic should live in application code where it can be properly monitored and logged.
9. Version Compatibility: This restriction exists across all modern MySQL versions (5.7, 8.0, 8.1). MariaDB has similar restrictions with some extensions.
10. Debugging Tips: When error 1422 appears, check not just the immediate code, but all procedures/functions called within the trigger. The problematic statement might be nested several levels deep.
EE_WRITE (3): Error writing file
How to fix "EE_WRITE (3): Error writing file" in MySQL
CR_PARAMS_NOT_BOUND (2031): No data supplied for parameters
How to fix "CR_PARAMS_NOT_BOUND (2031): No data supplied for parameters" 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