From 3adc38e3f97eda29801e347a852a059e4c3f508c Mon Sep 17 00:00:00 2001 From: Alexander Early Date: Tue, 13 Jan 2015 00:02:30 -0800 Subject: [PATCH 1/5] working on removing deferral from auto --- lib/async.js | 55 ++++++++++++++++++++++++++-------------------- test/test-async.js | 51 +++++++++++++++++++++++++++++------------- 2 files changed, 67 insertions(+), 39 deletions(-) diff --git a/lib/async.js b/lib/async.js index 394c41cad..1d62bf099 100644 --- a/lib/async.js +++ b/lib/async.js @@ -416,45 +416,46 @@ async.auto = function (tasks, callback) { callback = callback || function () {}; var keys = _keys(tasks); - var remainingTasks = keys.length - if (!remainingTasks) { + if (keys.length === 0) { return callback(); } var results = {}; - + var running = {}; var listeners = []; - var addListener = function (fn) { + + function addListener(fn) { listeners.unshift(fn); - }; - var removeListener = function (fn) { + } + function removeListener(fn) { for (var i = 0; i < listeners.length; i += 1) { if (listeners[i] === fn) { listeners.splice(i, 1); return; } } - }; - var taskComplete = function () { - remainingTasks-- + } + function taskComplete() { _each(listeners.slice(0), function (fn) { fn(); }); - }; + } - addListener(function () { - if (!remainingTasks) { + function allComplete() { + if (listeners.length === 1) { var theCallback = callback; // prevent final callback from calling itself if it errors callback = function () {}; - + removeListener(allComplete); theCallback(null, results); } - }); + } + + addListener(allComplete); _each(keys, function (k) { var task = _isArray(tasks[k]) ? tasks[k]: [tasks[k]]; - var taskCallback = function (err) { + function taskCallback(err) { var args = Array.prototype.slice.call(arguments, 1); if (args.length <= 1) { args = args[0]; @@ -465,27 +466,35 @@ safeResults[rkey] = results[rkey]; }); safeResults[k] = args; - callback(err, safeResults); + + var theCallback = callback; // stop subsequent errors hitting callback multiple times callback = function () {}; + theCallback(err, safeResults); } else { results[k] = args; - async.setImmediate(taskComplete); + taskComplete(); } - }; + } var requires = task.slice(0, Math.abs(task.length - 1)) || []; - var ready = function () { + + function ready() { return _reduce(requires, function (a, x) { return (a && results.hasOwnProperty(x)); }, true) && !results.hasOwnProperty(k); - }; + } + if (ready()) { task[task.length - 1](taskCallback, results); } else { - var listener = function () { + var listener = function listener() { + if (running[k]) { + return; + } if (ready()) { + running[k] = true; removeListener(listener); task[task.length - 1](taskCallback, results); } @@ -550,9 +559,7 @@ else { args.push(callback); } - async.setImmediate(function () { - iterator.apply(null, args); - }); + iterator.apply(null, args); } }; }; diff --git a/test/test-async.js b/test/test-async.js index 2d46b8cef..48379a7bc 100755 --- a/test/test-async.js +++ b/test/test-async.js @@ -619,7 +619,7 @@ exports['retry as an embedded task'] = function(test) { var retryResult = 'RETRY'; var fooResults; var retryResults; - + async.auto({ foo: function(callback, results){ fooResults = results; @@ -687,23 +687,44 @@ exports['waterfall no callback'] = function(test){ ]); }; -exports['waterfall async'] = function(test){ +exports['waterfall no deferral'] = function(test){ var call_order = []; async.waterfall([ function(callback){ call_order.push(1); callback(); + // the final callback will return before we get here call_order.push(2); }, function(callback){ call_order.push(3); callback(); + } + ], + function(){ + test.same(call_order, [1,3]); + test.done(); + }); +}; + + +exports['waterfall async'] = function(test){ + var call_order = []; + async.waterfall([ + function(callback){ + call_order.push(1); + async.nextTick(callback); + call_order.push(2); }, - function(){ - test.same(call_order, [1,2,3]); - test.done(); + function(callback){ + call_order.push(3); + callback(); } - ]); + ], + function(){ + test.same(call_order, [1,2,3]); + test.done(); + }); }; exports['waterfall error'] = function(test){ @@ -743,7 +764,7 @@ exports['waterfall multiple callback calls'] = function(test){ call_order.push(4); arr[3] = function(){ call_order.push(4); - test.same(call_order, [1,2,2,3,3,4,4]); + test.same(call_order, [1,2,3,4,2,3,4]); test.done(); }; } @@ -2806,20 +2827,20 @@ exports['cargo bulk task'] = function (test) { }; exports['cargo drain once'] = function (test) { - + var c = async.cargo(function (tasks, callback) { callback(); }, 3); - + var drainCounter = 0; c.drain = function () { drainCounter++; } - + for(var i = 0; i < 10; i++){ c.push(i); } - + setTimeout(function(){ test.equal(drainCounter, 1); test.done(); @@ -2827,17 +2848,17 @@ exports['cargo drain once'] = function (test) { }; exports['cargo drain twice'] = function (test) { - + var c = async.cargo(function (tasks, callback) { callback(); }, 3); - + var loadCargo = function(){ for(var i = 0; i < 10; i++){ c.push(i); } }; - + var drainCounter = 0; c.drain = function () { drainCounter++; @@ -3161,7 +3182,7 @@ exports['queue started'] = function(test) { var calls = []; var q = async.queue(function(task, cb) {}); - + test.equal(q.started, false); q.push([]); test.equal(q.started, true); From bd9d033df340f00a79119bb4001b0aefa8bb6bf0 Mon Sep 17 00:00:00 2001 From: Alexander Early Date: Sun, 18 Jan 2015 01:02:07 -0800 Subject: [PATCH 2/5] fixed all auto tests --- lib/async.js | 37 ++++++++++++++++++------------------- test/test-async.js | 30 ++++++++++++++---------------- 2 files changed, 32 insertions(+), 35 deletions(-) diff --git a/lib/async.js b/lib/async.js index 1d62bf099..7da26f77c 100644 --- a/lib/async.js +++ b/lib/async.js @@ -425,7 +425,7 @@ var listeners = []; function addListener(fn) { - listeners.unshift(fn); + listeners.push(fn); } function removeListener(fn) { for (var i = 0; i < listeners.length; i += 1) { @@ -442,7 +442,7 @@ } function allComplete() { - if (listeners.length === 1) { + if (listeners.length === 1 && _keys(running).length === 0) { var theCallback = callback; // prevent final callback from calling itself if it errors callback = function () {}; @@ -451,11 +451,11 @@ } } - addListener(allComplete); _each(keys, function (k) { var task = _isArray(tasks[k]) ? tasks[k]: [tasks[k]]; function taskCallback(err) { + delete running[k]; var args = Array.prototype.slice.call(arguments, 1); if (args.length <= 1) { args = args[0]; @@ -485,23 +485,22 @@ }, true) && !results.hasOwnProperty(k); } - if (ready()) { - task[task.length - 1](taskCallback, results); - } - else { - var listener = function listener() { - if (running[k]) { - return; - } - if (ready()) { - running[k] = true; - removeListener(listener); - task[task.length - 1](taskCallback, results); - } - }; - addListener(listener); - } + var listener = function listener() { + if (running[k]) { + return; + } + if (ready()) { + running[k] = true; + removeListener(listener); + task[task.length - 1](taskCallback, results); + } + }; + addListener(listener); }); + + addListener(allComplete); + + taskComplete(); }; async.retry = function(times, task, callback) { diff --git a/test/test-async.js b/test/test-async.js index 48379a7bc..6e637f27b 100755 --- a/test/test-async.js +++ b/test/test-async.js @@ -384,7 +384,7 @@ exports['auto'] = function(test){ }] }, function(err){ - test.same(callOrder, ['task2','task6','task3','task5','task1','task4']); + test.same(callOrder, ['task2','task3','task6','task5','task1','task4']); test.done(); }); }; @@ -489,6 +489,7 @@ exports['auto no callback'] = function(test){ }; exports['auto error should pass partial results'] = function(test) { + test.expect(3); async.auto({ task1: function(callback){ callback(false, 'result1'); @@ -501,6 +502,7 @@ exports['auto error should pass partial results'] = function(test) { }] }, function(err, results){ + console.log("auto error done") test.equals(err, 'testerror'); test.equals(results.task1, 'result1'); test.equals(results.task2, 'result2'); @@ -520,20 +522,9 @@ exports['auto removeListener has side effect on loop iterator'] = function(test) // Issue 410 on github: https://github.com/caolan/async/issues/410 exports['auto calls callback multiple times'] = function(test) { - if (typeof process === 'undefined') { - // node only test - test.done(); - return; - } var finalCallCount = 0; - var domain = require('domain').create(); - domain.on('error', function (e) { - // ignore test error - if (!e._test_error) { - return test.done(e); - } - }); - domain.run(function () { + // this will actually be synchronous, we can use try/catch + try { async.auto({ task1: function(callback) { callback(null); }, task2: ['task1', function(callback) { callback(null); }] @@ -546,7 +537,12 @@ exports['auto calls callback multiple times'] = function(test) { e._test_error = true; throw e; }); - }); + } catch (e) { + // ignore test error + if (!e._test_error) { + return test.done(e); + } + } setTimeout(function () { test.equal(finalCallCount, 1, "Final auto callback should only be called once" @@ -574,8 +570,10 @@ exports['auto modifying results causes final callback to run early'] = function( } }, function(err, results){ + debugger; test.equal(results.inserted, true) - test.ok(results.task3, 'task3') + test.ok(results.task2, 'task2 should be called') + test.ok(results.task3, 'task3 should be called') test.done(); }); }; From 9cd8a7778bcf3d4c75cf355cfd0537332c7a3be7 Mon Sep 17 00:00:00 2001 From: Alexander Early Date: Mon, 19 Jan 2015 17:33:31 -0800 Subject: [PATCH 3/5] removed deferrals from queue, subtle breaking changes --- lib/async.js | 66 +++++++++++++++++++++--------------------- test/test-async.js | 71 +++++++++++++++++++++++++++------------------- 2 files changed, 75 insertions(+), 62 deletions(-) diff --git a/lib/async.js b/lib/async.js index 7da26f77c..20e107f72 100644 --- a/lib/async.js +++ b/lib/async.js @@ -33,6 +33,8 @@ } } + function noop () {} + //// cross-browser compatiblity functions //// var _toString = Object.prototype.toString; @@ -537,8 +539,9 @@ async.waterfall = function (tasks, callback) { callback = callback || function () {}; if (!_isArray(tasks)) { - var err = new Error('First argument to waterfall must be an array of functions'); - return callback(err); + // would it be better to throw this? + return callback(new Error('First argument to waterfall must be ' + + 'an array of functions')); } if (!tasks.length) { return callback(); @@ -738,6 +741,9 @@ if (concurrency === undefined) { concurrency = 1; } + + var running = 0; + function _insert(q, data, pos, callback) { if (!q.started){ q.started = true; @@ -745,18 +751,14 @@ if (!_isArray(data)) { data = [data]; } - if(data.length == 0) { - // call drain immediately if there are no tasks - return async.setImmediate(function() { - if (q.drain) { - q.drain(); - } - }); + if(data.length === 0 && q.idle()) { + // call drain immediately if there are no tasks + q.drain(); } _each(data, function(task) { var item = { data: task, - callback: typeof callback === 'function' ? callback : null + callback: typeof callback === 'function' ? callback : noop }; if (pos) { @@ -765,45 +767,43 @@ q.tasks.push(item); } - if (q.saturated && q.tasks.length === q.concurrency) { - q.saturated(); - } - async.setImmediate(q.process); + q.process(); }); } - var workers = 0; var q = { tasks: [], concurrency: concurrency, - saturated: null, - empty: null, - drain: null, + saturated: noop, + empty: noop, + drain: noop, started: false, paused: false, push: function (data, callback) { _insert(q, data, false, callback); }, kill: function () { - q.drain = null; + q.drain = noop; q.tasks = []; + running = 0; }, unshift: function (data, callback) { _insert(q, data, true, callback); }, process: function () { - if (!q.paused && workers < q.concurrency && q.tasks.length) { + if (!q.paused && running < q.concurrency && q.tasks.length) { var task = q.tasks.shift(); - if (q.empty && q.tasks.length === 0) { - q.empty(); + running += 1; + if (running === q.concurrency) { + q.saturated(); } - workers += 1; var next = function () { - workers -= 1; - if (task.callback) { - task.callback.apply(task, arguments); + running -= 1; + if (running === 0) { + q.empty(); } - if (q.drain && q.tasks.length + workers === 0) { + task.callback.apply(task, arguments); + if (q.tasks.length + running === 0) { q.drain(); } q.process(); @@ -816,10 +816,10 @@ return q.tasks.length; }, running: function () { - return workers; + return running; }, idle: function() { - return q.tasks.length + workers === 0; + return q.tasks.length + running === 0; }, pause: function () { if (q.paused === true) { return; } @@ -830,8 +830,8 @@ q.paused = false; // Need to call q.process once per concurrent // worker to preserve full concurrency after pause - for (var w = 1; w <= q.concurrency; w++) { - async.setImmediate(q.process); + for (var w = 0; w < q.concurrency; w++) { + q.process(); } } }; @@ -842,7 +842,7 @@ function _compareTasks(a, b){ return a.priority - b.priority; - }; + } function _binarySearch(sequence, item, compare) { var beg = -1, @@ -882,7 +882,7 @@ q.tasks.splice(_binarySearch(q.tasks, item, _compareTasks) + 1, 0, item); - if (q.saturated && q.tasks.length === q.concurrency) { + if (q.tasks.length === q.concurrency) { q.saturated(); } async.setImmediate(q.process); diff --git a/test/test-async.js b/test/test-async.js index 6e637f27b..4ddbad9a2 100755 --- a/test/test-async.js +++ b/test/test-async.js @@ -449,7 +449,12 @@ exports['auto results'] = function(test){ }, function(err, results){ test.same(callOrder, ['task2','task3','task1','task4']); - test.same(results, {task1: ['task1a','task1b'], task2: 'task2', task3: undefined, task4: 'task4'}); + test.same(results, { + task1: ['task1a','task1b'], + task2: 'task2', + task3: undefined, + task4: 'task4' + }); test.done(); }); }; @@ -570,7 +575,6 @@ exports['auto modifying results causes final callback to run early'] = function( } }, function(err, results){ - debugger; test.equal(results.inserted, true) test.ok(results.task2, 'task2 should be called') test.ok(results.task3, 'task3 should be called') @@ -1402,7 +1406,8 @@ exports['mapLimit empty array'] = function(test){ exports['mapLimit limit exceeds size'] = function(test){ var call_order = []; - async.mapLimit([0,1,2,3,4,5,6,7,8,9], 20, mapIterator.bind(this, call_order), function(err, results){ + async.mapLimit([0,1,2,3,4,5,6,7,8,9], 20, mapIterator.bind(this, call_order), + function(err, results){ test.same(call_order, [0,1,2,3,4,5,6,7,8,9]); test.same(results, [0,2,4,6,8,10,12,14,16,18]); test.done(); @@ -1411,7 +1416,8 @@ exports['mapLimit limit exceeds size'] = function(test){ exports['mapLimit limit equal size'] = function(test){ var call_order = []; - async.mapLimit([0,1,2,3,4,5,6,7,8,9], 10, mapIterator.bind(this, call_order), function(err, results){ + async.mapLimit([0,1,2,3,4,5,6,7,8,9], 10, mapIterator.bind(this, call_order), + function(err, results){ test.same(call_order, [0,1,2,3,4,5,6,7,8,9]); test.same(results, [0,2,4,6,8,10,12,14,16,18]); test.done(); @@ -2159,7 +2165,7 @@ exports['queue'] = function (test) { setTimeout(function () { call_order.push('process ' + task); callback('error', 'arg'); - }, delays.splice(0,1)[0]); + }, delays.shift()); }, 2); q.push(1, function (err, arg) { @@ -2186,7 +2192,7 @@ exports['queue'] = function (test) { test.equal(q.length(), 0); call_order.push('callback ' + 4); }); - test.equal(q.length(), 4); + test.equal(q.length(), 2); test.equal(q.concurrency, 2); q.drain = function () { @@ -2212,7 +2218,7 @@ exports['queue default concurrency'] = function (test) { setTimeout(function () { call_order.push('process ' + task); callback('error', 'arg'); - }, delays.splice(0,1)[0]); + }, delays.shift()); }); q.push(1, function (err, arg) { @@ -2239,7 +2245,7 @@ exports['queue default concurrency'] = function (test) { test.equal(q.length(), 0); call_order.push('callback ' + 4); }); - test.equal(q.length(), 4); + test.equal(q.length(), 3); test.equal(q.concurrency, 1); q.drain = function () { @@ -2262,10 +2268,6 @@ exports['queue error propagation'] = function(test){ callback(task.name === 'foo' ? new Error('fooError') : null); }, 2); - q.drain = function() { - test.deepEqual(results, ['bar', 'fooError']); - test.done(); - }; q.push({name: 'bar'}, function (err) { if(err) { @@ -2284,6 +2286,11 @@ exports['queue error propagation'] = function(test){ results.push('foo'); }); + + async.nextTick(function () { + test.deepEqual(results, ['bar', 'fooError']); + test.done(); + }) }; exports['queue changing concurrency'] = function (test) { @@ -2297,9 +2304,12 @@ exports['queue changing concurrency'] = function (test) { setTimeout(function () { call_order.push('process ' + task); callback('error', 'arg'); - }, delays.splice(0,1)[0]); + }, delays.shift()); }, 2); + test.equal(q.concurrency, 2); + q.concurrency = 1; + q.push(1, function (err, arg) { test.equal(err, 'error'); test.equal(arg, 'arg'); @@ -2324,9 +2334,7 @@ exports['queue changing concurrency'] = function (test) { test.equal(q.length(), 0); call_order.push('callback ' + 4); }); - test.equal(q.length(), 4); - test.equal(q.concurrency, 2); - q.concurrency = 1; + test.equal(q.length(), 3); setTimeout(function () { test.same(call_order, [ @@ -2377,16 +2385,18 @@ exports['queue unshift'] = function (test) { var q = async.queue(function (task, callback) { queue_order.push(task); - callback(); + async.nextTick(callback); }, 1); + // this will immediately start running q.unshift(4); + // these will run later q.unshift(3); q.unshift(2); q.unshift(1); setTimeout(function () { - test.same(queue_order, [ 1, 2, 3, 4 ]); + test.same(queue_order, [ 4, 1, 2, 3 ]); test.done(); }, 100); }; @@ -2423,7 +2433,7 @@ exports['queue bulk task'] = function (test) { call_order.push('callback ' + arg); }); - test.equal(q.length(), 4); + test.equal(q.length(), 2); test.equal(q.concurrency, 2); setTimeout(function () { @@ -2443,7 +2453,7 @@ exports['queue idle'] = function(test) { var q = async.queue(function (task, callback) { // Queue is busy when workers are running test.equal(q.idle(), false) - callback(); + async.nextTick(callback); }, 1); // Queue is idle before anything added @@ -2567,22 +2577,23 @@ exports['queue pause with concurrency'] = function(test) { exports['queue kill'] = function (test) { var q = async.queue(function (task, callback) { setTimeout(function () { - test.ok(false, "Function should never be called"); + //test.ok(task === 0, "Function should not be called twice"); callback(); - }, 300); + }, 30); }, 1); q.drain = function() { - test.ok(false, "Function should never be called"); + //test.ok(false, "Function should never be called"); } - q.push(0); + debugger; + q.push([0, 1]); q.kill(); setTimeout(function() { test.equal(q.length(), 0); test.done(); - }, 600) + }, 60) }; exports['priorityQueue'] = function (test) { @@ -3117,11 +3128,11 @@ exports['queue events'] = function(test) { q.concurrency = 3; q.saturated = function() { - test.ok(q.length() == 3, 'queue should be saturated now'); + test.ok(q.running() == 3, 'queue should be saturated now'); calls.push('saturated'); }; q.empty = function() { - test.ok(q.length() == 0, 'queue should be empty now'); + test.ok(q.running() == 0, 'queue should be empty now'); calls.push('empty'); }; q.drain = function() { @@ -3131,17 +3142,19 @@ exports['queue events'] = function(test) { ); calls.push('drain'); test.same(calls, [ - 'saturated', 'process foo', 'process bar', + 'saturated', 'process zoo', 'foo cb', + 'saturated', 'process poo', 'bar cb', - 'empty', + 'saturated', 'process moo', 'zoo cb', 'poo cb', + 'empty', 'moo cb', 'drain' ]); From 72fc263a3480b6508ae14f3904e54864532b9642 Mon Sep 17 00:00:00 2001 From: Alexander Early Date: Mon, 19 Jan 2015 20:00:31 -0800 Subject: [PATCH 4/5] cleaned up priorityQueue and cargo --- lib/async.js | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/lib/async.js b/lib/async.js index 20e107f72..95fd850bf 100644 --- a/lib/async.js +++ b/lib/async.js @@ -865,13 +865,9 @@ if (!_isArray(data)) { data = [data]; } - if(data.length == 0) { - // call drain immediately if there are no tasks - return async.setImmediate(function() { - if (q.drain) { - q.drain(); - } - }); + if(data.length === 0 && q.idle()) { + // call drain immediately if there are no tasks + q.drain(); } _each(data, function(task) { var item = { @@ -882,9 +878,8 @@ q.tasks.splice(_binarySearch(q.tasks, item, _compareTasks) + 1, 0, item); - if (q.tasks.length === q.concurrency) { - q.saturated(); - } + // need to defer so tasks pushed on the same tick will + // prioritize properly async.setImmediate(q.process); }); } @@ -904,15 +899,15 @@ }; async.cargo = function (worker, payload) { - var working = false, - tasks = []; + var working = false, + tasks = []; var cargo = { tasks: tasks, payload: payload, - saturated: null, - empty: null, - drain: null, + saturated: noop, + empty: noop, + drain: noop, drained: true, push: function (data, callback) { if (!_isArray(data)) { @@ -921,19 +916,22 @@ _each(data, function(task) { tasks.push({ data: task, - callback: typeof callback === 'function' ? callback : null + callback: typeof callback === 'function' ? callback : noop }); cargo.drained = false; - if (cargo.saturated && tasks.length === payload) { + if (tasks.length === payload) { cargo.saturated(); } }); + + // need to defer so tasks pushed on the same tick will + // saturate the queue async.setImmediate(cargo.process); }, process: function process() { if (working) return; if (tasks.length === 0) { - if(cargo.drain && !cargo.drained) cargo.drain(); + if(!cargo.drained) cargo.drain(); cargo.drained = true; return; } @@ -946,7 +944,7 @@ return task.data; }); - if(cargo.empty) cargo.empty(); + cargo.empty(); working = true; worker(ds, function () { working = false; From 987e8c2e960ea759fd895a1b774fd8aae35251c7 Mon Sep 17 00:00:00 2001 From: Alexander Early Date: Mon, 19 Jan 2015 20:20:49 -0800 Subject: [PATCH 5/5] added notes in README about synchronous iterators and deferring callbacks --- README.md | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/README.md b/README.md index 420cd6a4c..f5ce04381 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,42 @@ missing please create a GitHub issue for it. ## Common Pitfalls + +### Synchronous iteration functions + +If you get an error like `RangeError: Maximum call stack size exceeded.` or other stack overflow issues when using async, you are likely using a synchronous iterator. By *synchronous* we mean a function that calls its callback on the same tick in the javascript event loop, without doing any I/O or using any timers. Calling many callbacks iteratively will quickly overflow the stack. If you run into this issue, just defer your callback with `async.nextTick` to start a new call stack on the next tick of the event loop. + +This can also arise by accident if you callback early in certain cases: + +```js +async.eachSeries(hugeArray, function iterator(item, callback) { + if (matchesSomething(item)) { + callback(null); // if many items match this, you'll overflow + } else { + doSomeIO(item, callback); + } +}, function done() { + //... +}); +``` + +Just change it to: + +```js +async.eachSeries(hugeArray, function iterator(item, callback) { + if (item.somePredicate()) { + async,nextTick(function () { + callback(null); + }); + } else { + doSomeIO(item, callback); + //... +``` + +Async does not guard against synchronous functions because it adds a small performance hit for each function call. Functions that are asynchronous by their nature do not have this problem and don't need the automatic callback deferral. + +If javascript's event loop is still a bit nebulous, check out [this article](http://blog.carbonfive.com/2013/10/27/the-javascript-event-loop-explained/) or [this talk](http://2014.jsconf.eu/speakers/philip-roberts-what-the-heck-is-the-event-loop-anyway.html) for more detailed information about how it works. + ### Binding a context to an iterator This section is really about `bind`, not about `async`. If you are wondering how to