Run the body once before testing the condition, with #do {# and #} while(…)#.
A retry schedule with exponential backoff: the first attempt always happens, and only the repeats depend on the condition. That asymmetry is the whole reason do…while exists.
<style>
body { font: 14px/1.5 system-ui, sans-serif; margin: 0; color: #24292f; }
ol { margin: 0; padding-left: 1.4em; }
li { padding: 3px 0; }
code { background: #f6f8fa; padding: 1px 5px; border-radius: 4px; font-size: 12px; }
.wait { color: #57606a; }
</style>
<script type="text/template" id="backoff">
#{ let attempt = 0; let wait = firstDelay; let total = 0; }#
<ol>
#do {#
#{ attempt++; }#
<li>
Attempt #attempt# to <code>#endpoint#</code>
#{ total += wait; }#
<span class="wait">— retry in #wait# ms</span>
#{ wait = wait * factor; }#
</li>
#} while(attempt < maxAttempts)#
</ol>
<p>Gives up after #attempt# attempts and #total# ms.</p>
</script>
<div id="output"></div>
<script src="https://cdn.jsdelivr.net/gh/richdafunk/hashJS@v1.3.6/hashJS.js"></script>
<script>
// A do-while runs its body before it tests the condition, which is exactly
// the shape of a retry: the first attempt is not a retry at all.
const data = {
endpoint: 'POST /v1/payments',
firstDelay: 250,
factor: 2,
maxAttempts: 5
};
new hashJS('backoff', data, 'output');
</script> The closing tag carries the condition: #} while(attempt < maxAttempts)#. It is recognised as a block continuation for the same reason #} else {# is — it begins with }.
Every number on screen is derived rather than supplied. The delay doubles through wait = wait * factor and the running total accumulates alongside it, so changing factor in the data changes the whole schedule without touching the template.