Introduction
The HTML5 ondrop event handler runs synchronously — it executes your code and returns immediately. When you use an asynchronous library like zip.js inside the drop handler to read or extract zip files, the handler returns before zip.js finishes processing. This causes problems because the DataTransfer object and File references from the drop event may become invalid after the handler returns. The solution is to read the file data synchronously (using FileReader or File.arrayBuffer()) within the event handler, and then pass that data to zip.js for async processing.
The Problem
1// BROKEN — ondrop returns before zip.js finishes
2document.getElementById('dropzone').ondrop = function(event) {
3 event.preventDefault();
4 const file = event.dataTransfer.files[0];
5
6 // zip.js is async — this starts but doesn't finish before ondrop returns
7 zip.createReader(new zip.BlobReader(file), function(reader) {
8 reader.getEntries(function(entries) {
9 console.log('Entries:', entries);
10 // By this point, the drop event is long finished
11 // file reference may be invalid in some browsers
12 });
13 });
14
15 // ondrop returns HERE — zip.js is still working
16 console.log('Handler returned');
17};
Fix: Read File Data First, Then Process
1const dropzone = document.getElementById('dropzone');
2
3dropzone.addEventListener('dragover', (e) => {
4 e.preventDefault(); // Required to allow drop
5 e.dataTransfer.dropEffect = 'copy';
6});
7
8dropzone.addEventListener('drop', async (e) => {
9 e.preventDefault();
10
11 const file = e.dataTransfer.files[0];
12 if (!file) return;
13
14 // Read the file data immediately (before the event handler scope closes)
15 const arrayBuffer = await file.arrayBuffer();
16
17 // Now process with zip.js using the captured data
18 try {
19 const reader = new zip.ZipReader(new zip.BlobReader(new Blob([arrayBuffer])));
20 const entries = await reader.getEntries();
21
22 for (const entry of entries) {
23 console.log(`File: ${entry.filename}, Size: ${entry.uncompressedSize}`);
24
25 if (!entry.directory) {
26 const text = await entry.getData(new zip.TextWriter());
27 console.log(`Content: ${text.substring(0, 100)}...`);
28 }
29 }
30
31 await reader.close();
32 } catch (error) {
33 console.error('Failed to process zip:', error);
34 }
35});
Modern zip.js API (v2+)
1dropzone.addEventListener('drop', async (e) => {
2 e.preventDefault();
3 const file = e.dataTransfer.files[0];
4
5 // zip.js v2+ supports BlobReader directly with async/await
6 const reader = new zip.ZipReader(new zip.BlobReader(file));
7
8 try {
9 const entries = await reader.getEntries();
10 const results = [];
11
12 for (const entry of entries) {
13 if (!entry.directory) {
14 // Extract as Blob
15 const blob = await entry.getData(new zip.BlobWriter());
16 results.push({
17 name: entry.filename,
18 size: entry.uncompressedSize,
19 blob: blob
20 });
21 }
22 }
23
24 displayResults(results);
25 } finally {
26 await reader.close();
27 }
28});
29
30function displayResults(files) {
31 const list = document.getElementById('file-list');
32 list.innerHTML = files.map(f =>
33 `<li>${f.name} (${(f.size / 1024).toFixed(1)} KB)</li>`
34 ).join('');
35}
1dropzone.addEventListener('drop', async (e) => {
2 e.preventDefault();
3 const file = e.dataTransfer.files[0];
4 const status = document.getElementById('status');
5
6 status.textContent = 'Reading zip file...';
7
8 const reader = new zip.ZipReader(new zip.BlobReader(file));
9 const entries = await reader.getEntries();
10 const total = entries.filter(e => !e.directory).length;
11 let processed = 0;
12
13 for (const entry of entries) {
14 if (!entry.directory) {
15 status.textContent = `Extracting ${++processed}/${total}: ${entry.filename}`;
16
17 const blob = await entry.getData(
18 new zip.BlobWriter(),
19 {
20 onprogress: (progress, total) => {
21 const pct = ((progress / total) * 100).toFixed(0);
22 status.textContent = `${entry.filename}: ${pct}%`;
23 }
24 }
25 );
26 }
27 }
28
29 await reader.close();
30 status.textContent = `Done! Extracted ${processed} files.`;
31});
Handling Multiple Dropped Files
1dropzone.addEventListener('drop', async (e) => {
2 e.preventDefault();
3 const files = Array.from(e.dataTransfer.files);
4
5 // Process each zip file
6 const zipFiles = files.filter(f => f.name.endsWith('.zip'));
7
8 for (const zipFile of zipFiles) {
9 const reader = new zip.ZipReader(new zip.BlobReader(zipFile));
10 const entries = await reader.getEntries();
11 console.log(`${zipFile.name}: ${entries.length} entries`);
12 await reader.close();
13 }
14});
Common Pitfalls
Not calling e.preventDefault() on dragover: The drop event only fires if dragover calls preventDefault(). Without it, the browser performs its default action (e.g., navigating to the file) and the drop handler never executes.
Accessing dataTransfer.files after the event handler returns: Some browsers invalidate the DataTransfer object after the event handler completes. Always read e.dataTransfer.files synchronously at the start of the handler and store references in local variables before any await.
Not handling non-zip files: Users may drop any file type. Check the file extension or MIME type before passing to zip.js. Attempting to read a non-zip file as a zip causes a confusing error from zip.js.
Memory issues with large zip files: Extracting large files into Blobs consumes memory proportional to the uncompressed size. For very large archives, use streaming extraction (zip.js TransformStream) or process entries one at a time instead of collecting all into an array.
Forgetting to close the ZipReader: Not calling reader.close() leaks resources (open file handles, Blob URLs). Always close the reader in a finally block to ensure cleanup even when extraction fails.
Summary
The ondrop handler returns synchronously — async operations like zip.js continue after it returns
Read file data or store file references at the start of the handler before any await
Use async/await with zip.js v2+ for clean, readable extraction code
Always call preventDefault() on both dragover and drop events
Close ZipReader in a finally block to prevent resource leaks