@@ -4,6 +4,7 @@ const common = require('../common');
44const assert = require ( 'assert' ) ;
55const fs = require ( 'fs' ) ;
66const path = require ( 'path' ) ;
7+ const test = require ( 'node:test' ) ;
78
89const tmpdir = require ( '../common/tmpdir' ) ;
910
@@ -287,3 +288,54 @@ doConcurrentAsyncMixedOps().then(common.mustCall());
287288 dir . closeSync ( ) ;
288289 assert . rejects ( dir . close ( ) , dirclosedError ) . then ( common . mustCall ( ) ) ;
289290}
291+
292+ test ( 'fs.opendir should not double callback if user callback throws' , async ( t ) => {
293+ let readCallbackInvokedCount = 0 ;
294+ let userErrorThrown = false ;
295+
296+ const dir = await fs . promises . opendir ( '.' ) ;
297+
298+ t . after ( ( ) => dir . close ( ) . catch ( ( err ) => console . error ( 'Error closing dir in t.after:' , err ) ) ) ;
299+
300+ await new Promise ( ( resolve , reject ) => {
301+ const operationTimeout = setTimeout ( ( ) => {
302+ if ( userErrorThrown && readCallbackInvokedCount === 1 ) {
303+ resolve ( ) ; // User error handled, no double callback
304+ } else if ( readCallbackInvokedCount === 0 ) {
305+ reject ( new Error ( 'dir.read callback was never invoked.' ) ) ;
306+ } else {
307+ reject ( new Error ( 'Test timeout reached in an unexpected state.' ) ) ;
308+ }
309+ } , 1000 ) ;
310+
311+ dir . read ( ( err , dirent ) => {
312+ readCallbackInvokedCount ++ ;
313+
314+ if ( err ) {
315+ // This is an error from dir.read() itself, not the user error we're testing.
316+ clearTimeout ( operationTimeout ) ;
317+ return reject ( new Error ( `dir.read reported an error: ${ err . message } ` ) ) ;
318+ }
319+
320+ if ( readCallbackInvokedCount === 1 ) {
321+ userErrorThrown = true ;
322+ // Simulate the user's code throwing an error.
323+ // A robust fs.Dir should catch this internally and not call this callback again.
324+ throw new Error ( 'Simulated user error in dir.read callback' ) ;
325+ }
326+
327+ if ( readCallbackInvokedCount > 1 ) {
328+ clearTimeout ( operationTimeout ) ;
329+ return reject ( new Error ( 'dir.read callback was invoked multiple times.' ) ) ;
330+ }
331+
332+ // If dirent is null and it's the first call, and no user error was meant to be thrown yet,
333+ // it implies an empty directory or an issue with the test setup if we expected more entries.
334+ // For this specific test, we expect the user error to be thrown on the first entry.
335+ if ( dirent === null && readCallbackInvokedCount === 1 && ! userErrorThrown ) {
336+ clearTimeout ( operationTimeout ) ;
337+ return reject ( new Error ( 'Directory reading finished before user error could be simulated.' ) ) ;
338+ }
339+ } ) ;
340+ } ) ;
341+ } ) ;
0 commit comments