The Exact Error
Error: Invalid salt version
Error: data and hash arguments required
Error: Invalid hash provided to bcrypt
Error: data must not be empty
Or the silent failure:
const match = await bcrypt.compare(password, hash);
// match === false (even though the password is correct)
Quick summary: bcrypt errors come from passing the wrong arguments ā an empty string, a non-bcrypt hash, or comparing an already-hashed password against a stored hash.
Why This Error Happens
1. Hash stored incorrectly ā truncated column, encoding mismatch, or the stored value is not a bcrypt hash
2. Double hashing ā password hashed before being passed to bcrypt.compare(), comparing hash-vs-hash
3. Empty input ā bcrypt.hash(undefined) or bcrypt.hash('') depending on version
4. Wrong library ā mixing bcryptjs (pure JS) and bcrypt (native) hashes
5. Hash truncation ā database column too short (e.g., VARCHAR(50) instead of VARCHAR(60))
Step-by-Step Diagnosis
Step 1 ā Inspect the stored hash
console.log(hash);
// Valid bcrypt hash looks like:
// $2b$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy
// Must be exactly 60 characters, start with $2a$, $2b$, or $2y$
console.log(hash.length); // Should be 60
console.log(hash.startsWith('$2'));
Step 2 ā Check the registration code for double hashing
// WRONG ā hashing the password before storage AND before compare
app.post('/login', async (req, res) => {
const inputHash = await bcrypt.hash(req.body.password, 10); // Don't do this!
const match = await bcrypt.compare(inputHash, user.hash);
});
// RIGHT
app.post('/login', async (req, res) => {
const match = await bcrypt.compare(req.body.password, user.hash);
});
Step 3 ā Check for empty values
if (!password) throw new Error('Password is required');
if (!hash) throw new Error('Hash is required');
const match = await bcrypt.compare(password, hash);
Solutions
Solution 1 ā Fix database column length
-- bcrypt hashes are always 60 characters
ALTER TABLE users MODIFY COLUMN password_hash VARCHAR(60) NOT NULL;
Solution 2 ā Only hash on write, compare plain on read
// Registration (write path) ā hash once
const hash = await bcrypt.hash(plainPassword, 10);
await db.query('INSERT INTO users (password_hash) VALUES (?)', [hash]);
// Login (read path) ā compare plain password
const storedHash = user.password_hash;
const match = await bcrypt.compare(plainPassword, storedHash);
Solution 3 ā Validate before hashing
async function hashPassword(plain) {
if (!plain || typeof plain !== 'string') {
throw new Error('Password must be a non-empty string');
}
return bcrypt.hash(plain, 10);
}
async function verifyPassword(plain, hash) {
if (!plain || !hash) return false;
if (!hash.startsWith('$2')) return false;
return bcrypt.compare(plain, hash);
}
Real-World Examples
Express + Mongoose registration:
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
this.password = await bcrypt.hash(this.password, 12);
next();
});
// On login ā do NOT hash again
userSchema.methods.comparePassword = async function(candidate) {
return bcrypt.compare(candidate, this.password);
};
Quick Reference ā bcrypt Error Causes
| Error | Most Likely Cause |
|---|---|
| Invalid salt version | Hash is not bcrypt (wrong prefix) |
| data and hash required | Passing undefined/null |
| match === false (unexpectedly) | Double-hashing the password |
| Invalid hash provided | Hash is truncated or corrupted |
| Hash too long | Input exceeds 72 bytes (bcrypt limit) |
Prevent This Error in the Future
1. Use VARCHAR(60) for hash columns ā bcrypt output is always exactly 60 characters.
2. Never pre-hash before compare() ā only the plain password goes into compare().
3. Log the hash on registration during development to confirm it looks correct.
Use ToolNinja to Debug Faster
The Hash Generator lets you create bcrypt hashes and verify them interactively ā useful for testing the correct round count and verifying that a known password matches a stored hash.