User-defined Iterables

Give a class a [Symbol.iterator]() method and every consumer accepts it. Writing the method as a generator (*[Symbol.iterator]()) is shortest: the engine builds next() for you, plus a return() that runs your finally block when a loop exits early, the place to release file handles or subscriptions.

An iterable Playlist class with cleanup on early exitJavaScriptLive
class Playlist {
  constructor(...songs) { this.songs = songs; }
  *[Symbol.iterator]() {                        // the generator supplies next() and return()
    try { yield* this.songs; } finally { console.log('closed'); }
  }
}
const list = new Playlist('Intro', 'Verse', 'Chorus');
console.log([...list], Array.from(list, s => s.length));
for (const song of list) if (song === 'Verse') break;       // break calls return()

Keep iteration state inside the iterator, never on the collection: an index stored in this is not reset, so a second loop would yield nothing.