Asynchronous testing capabilities for Promise rejection scenarios.
Asserts that the Promise rejects with an exception. Can optionally specify the type of error and message pattern.
/**
* Asserts that the Promise rejects with an exception
* @param {Function} [type] - Constructor function the error must be instance of
* @param {string|RegExp} [message] - String or regex the error message must match
* @returns {Promise<*>} Promise resolving to the rejected error object
*/
reject(type, message)Alias:
rejects()Basic Promise Rejection Testing:
const Code = require('@hapi/code');
const expect = Code.expect;
// Promise that rejects
const rejectingPromise = Promise.reject(new Error('Something went wrong'));
// Basic rejection assertion
await expect(rejectingPromise).to.reject();
// Promise that resolves
const resolvingPromise = Promise.resolve('success');
await expect(resolvingPromise).to.not.reject();Error Type Checking:
// Check specific error type
const typeErrorPromise = Promise.reject(new TypeError('Invalid type'));
const rangeErrorPromise = Promise.reject(new RangeError('Out of range'));
await expect(typeErrorPromise).to.reject(TypeError);
await expect(rangeErrorPromise).to.reject(RangeError);
await expect(typeErrorPromise).to.reject(Error); // Parent class works
// Custom error types
class CustomError extends Error {
constructor(message) {
super(message);
this.name = 'CustomError';
}
}
const customErrorPromise = Promise.reject(new CustomError('Custom error'));
await expect(customErrorPromise).to.reject(CustomError);Message Validation:
// Exact message matching
const exactMessagePromise = Promise.reject(new Error('Exact error message'));
await expect(exactMessagePromise).to.reject('Exact error message');
// Regex message matching
const regexMessagePromise = Promise.reject(new Error('File not found: config.json'));
await expect(regexMessagePromise).to.reject(/File not found/);
await expect(regexMessagePromise).to.reject(/config\.json$/);
// Type and message combination
await expect(regexMessagePromise).to.reject(Error, /File not found/);Working with Rejected Errors:
// Capture and inspect rejected error
const errorPromise = Promise.reject((() => {
const err = new Error('Test error');
err.code = 'TEST_ERROR';
err.statusCode = 500;
return err;
})());
const rejectedError = await expect(errorPromise).to.reject();
// Further assertions on the rejected error
expect(rejectedError).to.be.an.instanceof(Error);
expect(rejectedError.message).to.equal('Test error');
expect(rejectedError.code).to.equal('TEST_ERROR');
expect(rejectedError.statusCode).to.equal(500);// Async function that throws
const asyncThrowingFunc = async (shouldThrow) => {
if (shouldThrow) {
throw new Error('Async operation failed');
}
return 'success';
};
// Test rejection
await expect(asyncThrowingFunc(true)).to.reject(Error, /operation failed/);
// Test success
await expect(asyncThrowingFunc(false)).to.not.reject();// Simulated API calls
const apiCall = async (endpoint) => {
if (endpoint === '/error') {
const error = new Error('API Error');
error.status = 500;
error.response = {code: 'INTERNAL_ERROR'};
throw error;
}
if (endpoint === '/notfound') {
const error = new Error('Not Found');
error.status = 404;
throw error;
}
return {data: 'success'};
};
// Test different error scenarios
const error500 = await expect(apiCall('/error')).to.reject();
expect(error500.status).to.equal(500);
expect(error500.response.code).to.equal('INTERNAL_ERROR');
const error404 = await expect(apiCall('/notfound')).to.reject();
expect(error404.status).to.equal(404);
// Test success case
await expect(apiCall('/success')).to.not.reject();// Complex promise chain
const processData = async (data) => {
return Promise.resolve(data)
.then(d => {
if (!d) throw new Error('No data provided');
return d;
})
.then(d => {
if (typeof d !== 'string') throw new TypeError('Data must be string');
return d.toUpperCase();
})
.then(d => {
if (d.length < 3) throw new RangeError('Data too short');
return d;
});
};
// Test different error conditions in the chain
await expect(processData(null)).to.reject(Error, /No data/);
await expect(processData(123)).to.reject(TypeError, /must be string/);
await expect(processData('hi')).to.reject(RangeError, /too short/);
// Test success
const result = await processData('hello world');
expect(result).to.equal('HELLO WORLD');// Promise that times out
const timeoutPromise = (ms, shouldReject = false) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (shouldReject) {
reject(new Error('Operation timed out'));
} else {
resolve('completed');
}
}, ms);
});
};
// Test timeout rejection
await expect(timeoutPromise(100, true)).to.reject(/timed out/);
// Test successful completion
await expect(timeoutPromise(1, false)).to.not.reject();
// Race condition testing
const fastPromise = Promise.resolve('fast');
const slowRejectingPromise = timeoutPromise(100, true);
const winner = await Promise.race([fastPromise, slowRejectingPromise]);
expect(winner).to.equal('fast');// Promise with fallback handling
const withFallback = async (primaryOperation, fallbackValue) => {
try {
return await primaryOperation();
} catch (error) {
if (error.code === 'RECOVERABLE') {
return fallbackValue;
}
throw error; // Re-throw non-recoverable errors
}
};
const recoverableError = () => {
const err = new Error('Recoverable error');
err.code = 'RECOVERABLE';
throw err;
};
const nonRecoverableError = () => {
const err = new Error('Fatal error');
err.code = 'FATAL';
throw err;
};
// Test recovery
const result1 = await withFallback(recoverableError, 'fallback');
expect(result1).to.equal('fallback');
// Test non-recoverable error
await expect(withFallback(nonRecoverableError, 'fallback'))
.to.reject(Error, /Fatal error/);// Testing Promise.all error handling
const promises = [
Promise.resolve('success1'),
Promise.reject(new Error('failure')),
Promise.resolve('success2')
];
await expect(Promise.all(promises)).to.reject(Error, /failure/);
// Testing Promise.allSettled
const settledResults = await Promise.allSettled(promises);
expect(settledResults[0].status).to.equal('fulfilled');
expect(settledResults[1].status).to.equal('rejected');
expect(settledResults[2].status).to.equal('fulfilled');
expect(settledResults[1].reason).to.be.an.instanceof(Error);
expect(settledResults[1].reason.message).to.equal('failure');// Special behavior with 'not' flag
const resolvingPromise = Promise.resolve('success');
const rejectingPromise = Promise.reject(new Error('error'));
// These work as expected
await expect(resolvingPromise).to.not.reject();
await expect(rejectingPromise).to.reject();
// With 'not', cannot specify error type or message
await expect(resolvingPromise).to.not.reject(); // OK
// await expect(resolvingPromise).to.not.reject(Error); // Error! Cannot specify args with 'not'
// The negated version returns null instead of error
const result = await expect(resolvingPromise).to.not.reject();
expect(result).to.be.null();// Database operation testing
const DatabaseError = class extends Error {
constructor(message, query) {
super(message);
this.name = 'DatabaseError';
this.query = query;
}
};
const dbQuery = async (sql) => {
if (sql.includes('DROP')) {
throw new DatabaseError('DROP operations not allowed', sql);
}
if (sql.includes('INVALID')) {
throw new DatabaseError('Invalid SQL syntax', sql);
}
return {rows: [], count: 0};
};
// Test database error scenarios
const dropError = await expect(dbQuery('DROP TABLE users'))
.to.reject(DatabaseError, /not allowed/);
expect(dropError.query).to.include('DROP TABLE');
await expect(dbQuery('SELECT * FROM users')).to.not.reject();
// File system operation testing
const fs = require('fs').promises;
// Test file not found
await expect(fs.readFile('/nonexistent/file.txt')).to.reject();
// Test permission denied (if running test with restricted permissions)
// await expect(fs.writeFile('/root/test.txt', 'data')).to.reject(/permission denied/i);