Working with CSV (Comma-Separated Values) files is a common task for JavaScript developers, whether you’re processing analytics data, importing user records, or handling spreadsheet exports. While it might be tempting to load an entire CSV file into memory at once, this approach quickly becomes problematic when dealing with files containing millions of rows or when your application needs to handle multiple file processing requests simultaneously.
This is where Node.js streams shine. Streams allow you to process data piece by piece, maintaining a small memory footprint regardless of file size. Instead of waiting for a 2GB CSV file to load completely into memory (which might crash your application), streams let you read and process it line by line or chunk by chunk.
In this comprehensive guide, we’ll build a CSV file reader from the ground up using Node.js streams. You’ll learn how streams work under the hood, understand the different types of streams available, and create a production-ready CSV parser that can handle files of any size efficiently. By the end of this article, you’ll have practical knowledge you can apply immediately to real-world data processing tasks.
Understanding Node.js Streams
Before we dive into building our CSV reader, let’s understand what streams are and why they’re essential for file processing. A stream is an abstract interface for working with streaming data in Node.js. Think of it like a conveyor belt in a factory: instead of waiting for all items to be manufactured before packaging them, workers can package items as they come off the production line.
Node.js provides four fundamental types of streams:
Readable streams allow you to read data from a source (like reading from a file).
Writable streams let you write data to a destination (like writing to a file).
Duplex streams are both readable and writable (like a network socket).
Transform streams are duplex streams that can modify data as it passes through (like compression).
For our CSV reader, we’ll primarily work with readable and transform streams. The readable stream will pull data from our CSV file, and transform streams will help us parse that data into usable JavaScript objects.
Here’s a simple example to illustrate how streams work:
const fs = require('fs');
// Create a readable stream from a file
const readableStream = fs.createReadStream('data.csv', {
encoding: 'utf8',
highWaterMark: 64 * 1024 // 64KB chunks
});
// Listen for data chunks
readableStream.on('data', (chunk) => {
console.log(`Received ${chunk.length} characters`);
});
// Listen for the end of the stream
readableStream.on('end', () => {
console.log('Finished reading file');
});
The createReadStream method creates a stream that reads the file in chunks of 64KB (specified by highWaterMark). Instead of loading the entire file at once, Node.js reads 64KB, emits a ‘data’ event, processes that chunk, then reads the next 64KB. This continues until the file is completely read, at which point the ‘end’ event fires. The memory usage stays constant regardless of whether the file is 1MB or 1GB.
Setting Up Your Project
Let’s create a new Node.js project for our CSV reader. Open your terminal and run these commands:
mkdir csv-stream-reader
cd csv-stream-reader
npm init -y
For this tutorial, we’ll build everything from scratch to understand the fundamentals, but we’ll also explore using the popular csv-parser library later for comparison. First, let’s create a sample CSV file to work with.
Create a file named users.csv:
id,name,email,age,city
1,John Doe,john@example.com,28,New York
2,Jane Smith,jane@example.com,34,Los Angeles
3,Bob Johnson,bob@example.com,45,Chicago
4,Alice Williams,alice@example.com,23,Houston
5,Charlie Brown,charlie@example.com,56,Phoenix
This simple dataset will help us test our CSV reader. Notice the first line contains headers, and each subsequent line represents a data row with values separated by commas.
Building a Basic Line-by-Line Reader
Before parsing CSV data, we need to read our file line by line. Streams naturally work with chunks of bytes, not lines, so we’ll need to split these chunks into individual lines.
Here’s our first implementation:
const fs = require('fs');
const { Transform } = require('stream');
// Transform stream that splits chunks into lines
class LineTransform extends Transform {
constructor(options) {
super(options);
this.buffer = '';
}
_transform(chunk, encoding, callback) {
// Add the new chunk to our buffer
this.buffer += chunk.toString();
// Split by newlines
const lines = this.buffer.split('n');
// Keep the last incomplete line in the buffer
this.buffer = lines.pop();
// Push complete lines downstream
lines.forEach(line => {
if (line.trim()) {
this.push(line + 'n');
}
});
callback();
}
_flush(callback) {
// Handle any remaining data in the buffer
if (this.buffer.trim()) {
this.push(this.buffer);
}
callback();
}
}
// Use our line transform
const readStream = fs.createReadStream('users.csv', { encoding: 'utf8' });
const lineTransform = new LineTransform();
readStream
.pipe(lineTransform)
.on('data', (line) => {
console.log('Line:', line);
})
.on('end', () => {
console.log('Finished reading all lines');
});
The Linetransform class extends Node.js’s `Transform` stream. When data chunks arrive via `_transform`, they might not align with line breaks. For example, one chunk might end with “John D” and the next start with “oe,john@example.com”. We maintain a `buffer` property that stores incomplete lines between chunks. When a new chunk arrives, we append it to the buffer, split by newlines, and keep the last element (which might be incomplete) in the buffer for the next iteration. The `_flush` method ensures we don’t lose the final line when the stream ends.
Expected output:
Line: id,name,email,age,city
Line: 1,John Doe,john@example.com,28,New York
Line: 2,Jane Smith,jane@example.com,34,Los Angeles
Line: 3,Bob Johnson,bob@example.com,45,Chicago
Line: 4,Alice Williams,alice@example.com,23,Houston
Line: 5,Charlie Brown,charlie@example.com,56,Phoenix
Finished reading all lines
Creating a CSV Parser Transform Stream
Now that we can read lines, let’s build a transform stream that parses CSV lines into JavaScript objects. This is where the real magic happens:
const { Transform } = require('stream');
class CSVParser extends Transform {
constructor(options = {}) {
super({ objectMode: true }); // We'll output objects, not strings
this.headers = null;
this.delimiter = options.delimiter || ',';
this.skipFirstLine = options.skipFirstLine !== false;
}
parseLine(line) {
const values = [];
let currentValue = '';
let insideQuotes = false;
for (let i = 0; i < line.length; i++) {
const char = line[i];
const nextChar = line[i + 1];
if (char === '"') {
if (insideQuotes && nextChar === '"') {
// Escaped quote
currentValue += '"';
i++; // Skip next quote
} else {
// Toggle quote state
insideQuotes = !insideQuotes;
}
} else if (char === this.delimiter && !insideQuotes) {
// End of field
values.push(currentValue.trim());
currentValue = '';
} else {
currentValue += char;
}
}
// Push the last value
values.push(currentValue.trim());
return values;
}
_transform(line, encoding, callback) {
line = line.toString().trim();
if (!line) {
callback();
return;
}
const values = this.parseLine(line);
if (!this.headers) {
// First line becomes headers
this.headers = values;
callback();
return;
}
// Create object from headers and values
const obj = {};
this.headers.forEach((header, index) => {
obj[header] = values[index] || '';
});
this.push(obj);
callback();
}
}
module.exports = CSVParser;
Understanding the parser: This parser handles several important CSV edge cases. The parseLine method processes each character, tracking whether we’re inside quoted values (which can contain commas that shouldn’t be treated as delimiters). It also handles escaped quotes (two consecutive quotes represent a single quote character).
The _transform method manages the parsing flow. The first line is stored as headers, then each subsequent line is converted into an object where header names become property keys.
Notice we set objectMode: true in the constructor. By default, streams work with strings or buffers, but object mode allows us to pass JavaScript objects through the stream pipeline.
Putting It All Together
Let’s combine our line reader and CSV parser to create a complete solution:
const fs = require('fs');
const { Transform, pipeline } = require('stream');
// LineTransform class from earlier
class LineTransform extends Transform {
constructor(options) {
super(options);
this.buffer = '';
}
_transform(chunk, encoding, callback) {
this.buffer += chunk.toString();
const lines = this.buffer.split('n');
this.buffer = lines.pop();
lines.forEach(line => {
if (line.trim()) {
this.push(line + 'n');
}
});
callback();
}
_flush(callback) {
if (this.buffer.trim()) {
this.push(this.buffer);
}
callback();
}
}
// CSVParser class from earlier
class CSVParser extends Transform {
constructor(options = {}) {
super({ objectMode: true });
this.headers = null;
this.delimiter = options.delimiter || ',';
}
parseLine(line) {
const values = [];
let currentValue = '';
let insideQuotes = false;
for (let i = 0; i < line.length; i++) {
const char = line[i];
const nextChar = line[i + 1];
if (char === '"') {
if (insideQuotes && nextChar === '"') {
currentValue += '"';
i++;
} else {
insideQuotes = !insideQuotes;
}
} else if (char === this.delimiter && !insideQuotes) {
values.push(currentValue.trim());
currentValue = '';
} else {
currentValue += char;
}
}
values.push(currentValue.trim());
return values;
}
_transform(line, encoding, callback) {
line = line.toString().trim();
if (!line) {
callback();
return;
}
const values = this.parseLine(line);
if (!this.headers) {
this.headers = values;
callback();
return;
}
const obj = {};
this.headers.forEach((header, index) => {
obj[header] = values[index] || '';
});
this.push(obj);
callback();
}
}
// Create our processing pipeline
function readCSV(filename, options = {}) {
const readStream = fs.createReadStream(filename, { encoding: 'utf8' });
const lineTransform = new LineTransform();
const csvParser = new CSVParser(options);
return pipeline(
readStream,
lineTransform,
csvParser,
(err) => {
if (err) {
console.error('Pipeline failed:', err);
}
}
);
}
// Use our CSV reader
const csvStream = readCSV('users.csv');
csvStream.on('data', (record) => {
console.log('Parsed record:', record);
});
csvStream.on('end', () => {
console.log('Finished processing CSV file');
});
Expected output:
Parsed record: { id: '1', name: 'John Doe', email: 'john@example.com', age: '28', city: 'New York' }
Parsed record: { id: '2', name: 'Jane Smith', email: 'jane@example.com', age: '34', city: 'Los Angeles' }
Parsed record: { id: '3', name: 'Bob Johnson', email: 'bob@example.com', age: '45', city: 'Chicago' }
Parsed record: { id: '4', name: 'Alice Williams', email: 'alice@example.com', age: '23', city: 'Houston' }
Parsed record: { id: '5', name: 'Charlie Brown', email: 'charlie@example.com', age: '56', city: 'Phoenix' }
Finished processing CSV file
Why use pipeline: The pipeline function automatically handles error propagation and cleanup. If any stream in the chain encounters an error, pipeline properly destroys all streams and calls the error callback. This prevents memory leaks and ensures proper resource cleanup.
Adding Data Transformation and Filtering
One of the most powerful aspects of streams is the ability to chain transformations. Let’s add filtering and data transformation capabilities:
const { Transform } = require('stream');
// Transform stream to filter records
class FilterTransform extends Transform {
constructor(filterFn, options) {
super({ objectMode: true, ...options });
this.filterFn = filterFn;
}
_transform(record, encoding, callback) {
if (this.filterFn(record)) {
this.push(record);
}
callback();
}
}
// Transform stream to modify records
class MapTransform extends Transform {
constructor(mapFn, options) {
super({ objectMode: true, ...options });
this.mapFn = mapFn;
}
_transform(record, encoding, callback) {
const transformed = this.mapFn(record);
this.push(transformed);
callback();
}
}
// Example usage
const { pipeline } = require('stream');
const fs = require('fs');
pipeline(
fs.createReadStream('users.csv', { encoding: 'utf8' }),
new LineTransform(),
new CSVParser(),
// Filter: only users older than 30
new FilterTransform(record => parseInt(record.age) > 30),
// Transform: convert age to number and add processed flag
new MapTransform(record => ({
...record,
age: parseInt(record.age),
processed: true,
processedAt: new Date().toISOString()
})),
(err) => {
if (err) console.error('Error:', err);
}
).on('data', (record) => {
console.log('Filtered and transformed:', record);
});
Expected output:
Filtered and transformed: {
id: '2',
name: 'Jane Smith',
email: 'jane@example.com',
age: 34,
city: 'Los Angeles',
processed: true,
processedAt: '2026-01-18T10:30:45.123Z'
}
Filtered and transformed: {
id: '3',
name: 'Bob Johnson',
email: 'bob@example.com',
age: 45,
city: 'Chicago',
processed: true,
processedAt: '2026-01-18T10:30:45.124Z'
}
Filtered and transformed: {
id: '5',
name: 'Charlie Brown',
email: 'charlie@example.com',
age: 56,
city: 'Phoenix',
processed: true,
processedAt: '2026-01-18T10:30:45.125Z'
}
We’ve created two reusable transform streams. FilterTransform only passes records that match a condition to the next stream. MapTransform modifies each record before passing it along. Notice how John (age 28) and Alice (age 23) don’t appear in the output because they didn’t pass the age filter.
The beauty of this approach is composability. You can chain as many transformations as needed without loading the entire dataset into memory.
Handling Large Files with Backpressure
When processing large files, you might write data slower than you read it (for example, writing to a database). Node.js streams implement backpressure to handle this automatically:
const { pipeline, Writable } = require('stream');
const fs = require('fs');
// Simulate slow processing (like database writes)
class SlowProcessor extends Writable {
constructor(options) {
super({ objectMode: true, ...options });
this.processedCount = 0;
}
async _write(record, encoding, callback) {
// Simulate slow async operation (database insert, API call, etc.)
await new Promise(resolve => setTimeout(resolve, 100));
this.processedCount++;
console.log(`Processed record ${this.processedCount}:`, record.name);
callback();
}
_final(callback) {
console.log(`Total records processed: ${this.processedCount}`);
callback();
}
}
// Process the CSV with backpressure handling
pipeline(
fs.createReadStream('users.csv', { encoding: 'utf8' }),
new LineTransform(),
new CSVParser(),
new SlowProcessor(),
(err) => {
if (err) {
console.error('Pipeline error:', err);
} else {
console.log('Pipeline completed successfully');
}
}
);
Expected output:
Processed record 1: John Doe
Processed record 2: Jane Smith
Processed record 3: Bob Johnson
Processed record 4: Alice Williams
Processed record 5: Charlie Brown
Total records processed: 5
Pipeline completed successfully
Understanding backpressure: Notice the 100ms delay in _write. In a real scenario, this might be a database insert or API call. The readable stream automatically pauses when the writable stream’s internal buffer fills up, preventing memory overflow. When the buffer drains, reading resumes automatically. You don’t need to manage this manually—Node.js handles it through the stream implementation.
Error Handling and Validation
Production code needs robust error handling. Let’s add validation and proper error management:
const { Transform } = require('stream');
class ValidatingCSVParser extends Transform {
constructor(schema, options = {}) {
super({ objectMode: true });
this.headers = null;
this.delimiter = options.delimiter || ',';
this.schema = schema;
this.lineNumber = 0;
this.errorCount = 0;
this.maxErrors = options.maxErrors || 10;
}
validateRecord(record) {
const errors = [];
for (const [field, rules] of Object.entries(this.schema)) {
const value = record[field];
if (rules.required && !value) {
errors.push(`${field} is required`);
}
if (rules.type === 'number' && value && isNaN(Number(value))) {
errors.push(`${field} must be a number`);
}
if (rules.type === 'email' && value && !value.includes('@')) {
errors.push(`${field} must be a valid email`);
}
if (rules.min && Number(value) < rules.min) {
errors.push(`${field} must be at least ${rules.min}`);
}
if (rules.max && Number(value) > rules.max) {
errors.push(`${field} must be at most ${rules.max}`);
}
}
return errors;
}
parseLine(line) {
const values = [];
let currentValue = '';
let insideQuotes = false;
for (let i = 0; i < line.length; i++) {
const char = line[i];
const nextChar = line[i + 1];
if (char === '"') {
if (insideQuotes && nextChar === '"') {
currentValue += '"';
i++;
} else {
insideQuotes = !insideQuotes;
}
} else if (char === this.delimiter && !insideQuotes) {
values.push(currentValue.trim());
currentValue = '';
} else {
currentValue += char;
}
}
values.push(currentValue.trim());
return values;
}
_transform(line, encoding, callback) {
this.lineNumber++;
line = line.toString().trim();
if (!line) {
callback();
return;
}
try {
const values = this.parseLine(line);
if (!this.headers) {
this.headers = values;
callback();
return;
}
const obj = {};
this.headers.forEach((header, index) => {
obj[header] = values[index] || '';
});
// Validate the record
const validationErrors = this.validateRecord(obj);
if (validationErrors.length > 0) {
this.errorCount++;
const error = new Error(
`Validation failed at line ${this.lineNumber}: ${validationErrors.join(', ')}`
);
error.record = obj;
error.validationErrors = validationErrors;
this.emit('validation-error', error);
if (this.errorCount > this.maxErrors) {
callback(new Error(`Too many validation errors (${this.errorCount})`));
return;
}
callback();
return;
}
this.push(obj);
callback();
} catch (err) {
callback(new Error(`Parse error at line ${this.lineNumber}: ${err.message}`));
}
}
}
// Example usage with validation
const schema = {
id: { required: true, type: 'number' },
name: { required: true },
email: { required: true, type: 'email' },
age: { required: true, type: 'number', min: 18, max: 120 }
};
const validatingParser = new ValidatingCSVParser(schema, { maxErrors: 5 });
validatingParser.on('validation-error', (error) => {
console.error('Validation Error:', error.message);
console.error('Invalid record:', error.record);
});
pipeline(
fs.createReadStream('users.csv', { encoding: 'utf8' }),
new LineTransform(),
validatingParser,
(err) => {
if (err) {
console.error('Fatal error:', err.message);
} else {
console.log('Processing completed successfully');
}
}
).on('data', (record) => {
console.log('Valid record:', record);
});
The `ValidatingCSVParser` checks each record against a schema. If validation fails, it emits a ‘validation-error’ event but continues processing (unless too many errors accumulate). This is useful in real applications where you want to log invalid records without stopping the entire import.
For our sample data, all records would pass validation. But if you had a record like `6,Tom,invalid-email,15,Boston`, you’d see:
Validation Error: Validation failed at line 7: email must be a valid email, age must be at least 18
Invalid record: { id: '6', name: 'Tom', email: 'invalid-email', age: '15', city: 'Boston' }
Writing Processed Data to a New CSV
Sometimes you need to write transformed data back to a CSV file. Here’s how to create a CSV writer stream:
const { Writable } = require('stream');
const fs = require('fs');
class CSVWriter extends Writable {
constructor(filename, headers, options = {}) {
super({ objectMode: true, ...options });
this.filename = filename;
this.headers = headers;
this.delimiter = options.delimiter || ',';
this.writeStream = fs.createWriteStream(filename, { encoding: 'utf8' });
this.headerWritten = false;
}
escapeValue(value) {
const stringValue = String(value);
// If value contains comma, newline, or quotes, wrap in quotes
if (stringValue.includes(this.delimiter) ||
stringValue.includes('n') ||
stringValue.includes('"')) {
return `"${stringValue.replace(/"/g, '""')}"`;
}
return stringValue;
}
_write(record, encoding, callback) {
if (!this.headerWritten) {
const headerLine = this.headers.join(this.delimiter) + 'n';
this.writeStream.write(headerLine);
this.headerWritten = true;
}
const values = this.headers.map(header => {
const value = record[header] !== undefined ? record[header] : '';
return this.escapeValue(value);
});
const line = values.join(this.delimiter) + 'n';
this.writeStream.write(line, callback);
}
_final(callback) {
this.writeStream.end(callback);
}
}
// Example: Read, transform, and write CSV
const outputHeaders = ['id', 'name', 'email', 'age', 'city', 'category'];
pipeline(
fs.createReadStream('users.csv', { encoding: 'utf8' }),
new LineTransform(),
new CSVParser(),
new MapTransform(record => ({
...record,
category: parseInt(record.age) >= 40 ? 'Senior' : 'Junior'
})),
new CSVWriter('output.csv', outputHeaders),
(err) => {
if (err) {
console.error('Error:', err);
} else {
console.log('Successfully wrote output.csv');
}
}
);
Result: This creates a new file `output.csv` with content:
id,name,email,age,city,category
1,John Doe,john@example.com,28,New York,Junior
2,Jane Smith,jane@example.com,34,Los Angeles,Junior
3,Bob Johnson,bob@example.com,45,Chicago,Senior
4,Alice Williams,alice@example.com,23,Houston,Junior
5,Charlie Brown,charlie@example.com,56,Phoenix,Senior
Key features: The escapeValue method handles CSV formatting rules—values with commas or quotes get wrapped in quotes, and internal quotes are escaped by doubling them. This ensures the output is valid CSV that can be read by spreadsheet applications.
Performance Monitoring and Statistics
Let’s add performance monitoring to track how efficiently we’re processing files:
const { Transform } = require('stream');
class StatisticsTransform extends Transform {
constructor(options = {}) {
super({ objectMode: true, ...options });
this.count = 0;
this.startTime = Date.now();
this.reportInterval = options.reportInterval || 1000;
this.lastReportTime = this.startTime;
this.lastReportCount = 0;
}
_transform(record, encoding, callback) {
this.count++;
const now = Date.now();
if (now - this.lastReportTime >= this.reportInterval) {
const totalElapsed = (now - this.startTime) / 1000;
const intervalElapsed = (now - this.lastReportTime) / 1000;
const intervalRecords = this.count - this.lastReportCount;
const overallRate = Math.round(this.count / totalElapsed);
const currentRate = Math.round(intervalRecords / intervalElapsed);
console.log(`Progress: ${this.count} records | Overall: ${overallRate} rec/sec | Current: ${currentRate} rec/sec`);
this.lastReportTime = now;
this.lastReportCount = this.count;
}
this.push(record);
callback();
}
_final(callback) {
const totalTime = (Date.now() - this.startTime) / 1000;
const avgRate = Math.round(this.count / totalTime);
console.log('n=== Processing Statistics ===');
console.log(`Total records: ${this.count}`);
console.log(`Total time: ${totalTime.toFixed(2)} seconds`);
console.log(`Average rate: ${avgRate} records/second`);
console.log(`Memory usage: ${Math.round(process.memoryUsage().heapUsed / 1024 / 1024)} MB`);
callback();
}
}
// Usage
pipeline(
fs.createReadStream('users.csv', { encoding: 'utf8' }),
new LineTransform(),
new CSVParser(),
new StatisticsTransform({ reportInterval: 1000 }),
new Writable({
objectMode: true,
write(record, encoding, callback) {
// Simulate processing
setTimeout(callback, 10);
}
}),
(err) => {
if (err) console.error('Error:', err);
}
);
**Expected output:**
Progress: 5 records | Overall: 5 rec/sec | Current: 5 rec/sec
=== Processing Statistics ===
Total records: 5
Total time: 1.05 seconds
Average rate: 5 records/second
Memory usage: 12 MB
Practical use: For large files with millions of records, this provides real-time feedback on processing speed and helps identify bottlenecks. If you notice the current rate dropping significantly, it might indicate your downstream processing (database writes, API calls) is slowing down.
Using the csv-parser Library
While building your own CSV parser is educational and gives you full control, production applications often benefit from battle-tested libraries. The csv-parser package is a popular choice:
npm install csv-parser
Here’s how to use it:
const fs = require('fs');
const csv = require('csv-parser');
const { pipeline } = require('stream');
const results = [];
pipeline(
fs.createReadStream('users.csv'),
csv({
mapHeaders: ({ header }) => header.trim().toLowerCase(),
mapValues: ({ value }) => value.trim()
}),
new Transform({
objectMode: true,
transform(record, encoding, callback) {
// Your custom processing
record.ageGroup = parseInt(record.age) >= 40 ? 'senior' : 'junior';
this.push(record);
callback();
}
}),
(err) => {
if (err) {
console.error('Pipeline failed:', err);
} else {
console.log('Processing complete');
}
}
).on('data', (data) => {
console.log('Record:', data);
});
**Expected output:**
Record: { id: '1', name: 'John Doe', email: 'john@example.com', age: '28', city: 'New York', ageGroup: 'junior' }
Record: { id: '2', name: 'Jane Smith', email: 'jane@example.com', age: '34', city: 'Los Angeles', ageGroup: 'junior' }
Record: { id: '3', name: 'Bob Johnson', email: 'bob@example.com', age: '45', city: 'Chicago', ageGroup: 'senior' }
Record: { id: '4', name: 'Alice Williams', email: 'alice@example.com', age: '23', city: 'Houston', ageGroup: 'junior' }
Record: { id: '5', name: 'Charlie Brown', email: 'charlie@example.com', age: '56', city: 'Phoenix', ageGroup: 'senior' }
Processing complete
Advantages of csv-parser: It handles edge cases like escaped quotes, different newline formats (CRLF vs LF), and various CSV dialects automatically. It’s well-tested and actively maintained. For production applications, this is often the better choice unless you have very specific parsing requirements.
Building a CSV file reader with Node.js streams teaches fundamental concepts that apply far beyond just parsing CSV files. You’ve learned how streams enable memory-efficient processing of large datasets, how to chain transformations for complex data pipelines, and how to handle errors gracefully in asynchronous workflows.
Your First CSV File Reader with Node.js Streams was originally published in Server-Side JavaScript on Medium, where people are continuing the conversation by highlighting and responding to this story.