[Javascript] Generator async runner example
Runner function:
function run(genFn) {
const it = genFn()
let state = { done: false, value: undefined }
step()
function step(isError, arg) {
try {
state = isError ? it.throw(arg) : it.next(arg)
} catch (e) {
// Unhandled in the generator -> surface it
console.error('Uncaught in generator:', e)
return
}
if (state.done) return
const v = state.value
// thenable detection
if (v && typeof v.then === 'function') {
v.then(
res => step(false, res),
err => step(true, err)
)
} else {
step(false, v)
}
}
}
Simple example for data fetching:
function* task() {
try {
const d = yield 1
console.log(d)
const resp = yield fetch('http://101.132.72.36:5100/api/local')
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
const result = yield resp.json()
console.log(result)
} catch (e) {
console.log('handled inside task:', e.message)
}
}
run(task)
Orchestrate dependent requests (without async/await
)
Good when you’re in old codebases or want to avoid transpilation.
function* createUserFlow(user) {
const created = yield fetch('/api/users', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(user),
})
const createdUser = yield created.json()
const profileResp = yield fetch(`/api/users/${createdUser.id}/profile`)
const profile = yield profileResp.json()
return { ...createdUser, profile }
}
run(function* () {
try {
const user = yield* createUserFlow({ name: 'Zhen' })
console.log('ready:', user)
} catch (e) {
console.error('flow failed', e)
}
})
Sequenced retries with backoff (clean, testable)
function sleep(ms) {
return new Promise(r => setTimeout(r, ms))
}
function* getWithRetry(url, tries = 3) {
let attempt = 0
while (attempt < tries) {
try {
const resp = yield fetch(url)
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
return yield resp.json()
} catch (e) {
attempt++
if (attempt >= tries) throw e
yield sleep(2 ** attempt * 200)
}
}
}
run(function* () {
const data = yield* getWithRetry('/api/data')
console.log(data)
})
Transaction-like steps with rollback
Great for UIs that must keep local/server state consistent.
function* updateWithRollback({ id, next, prev }) {
try {
yield fetch(`/api/items/${id}`, {
method: 'PUT',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(next),
})
return true
} catch (e) {
// rollback locally
yield Promise.resolve(prev) // pretend we reverted locally
return false
}
}
run(function* () {
const ok = yield* updateWithRollback({
id: 42,
next: { status: 'active' },
prev: { status: 'pending' },
})
console.log('commit?', ok)
})
Rate-limit a queue (simple job runner)
function* queueRunner(jobs, gapMs = 300) {
for (const job of jobs) {
yield job() // job returns a promise
yield new Promise(r => setTimeout(r, gapMs))
}
}
const jobs = Array.from({ length: 5 }, (_, i) => () =>
fetch(`/api/ping?i=${i}`))
run(function* () {
yield* queueRunner(jobs, 200)
console.log('all done')
})
TypeScript variant (typed runner, no deps)
// respects your style: tabs, single quotes, no semis
type Thenable<T = unknown> = { then: (onFulfilled: (v: T) => any, onRejected?: (e: any) => any) => any }
type Yieldable<T = unknown> = T | Thenable<T>
export function run<T>(genFn: () => Generator<Yieldable<any>, T, any>): void {
const it = genFn()
let state: IteratorResult<Yieldable<any>, T>
step(false, undefined)
function step(isError: boolean, arg: any): void {
try {
state = isError ? it.throw!(arg) : it.next(arg)
} catch (e) {
console.error('Uncaught in generator:', e)
return
}
if (state.done) return
const v = state.value as any
if (v && typeof v.then === 'function') {
;(v as Thenable).then((res: any) => step(false, res), (err: any) => step(true, err))
} else {
step(false, v)
}
}
}
Usage:
run(function* () {
const resp = yield fetch('/api/x')
if (!resp.ok) throw new Error('bad')
const data = yield resp.json()
console.log(data)
})
Should you still use this in 2025?
Usually: prefer async/await
— clearer, built-in, debug tools understand it. But this runner can still be handy when:
-
You’re touching legacy code that already uses generators
-
You want a dependency-free
co
-like runner for tiny bundles -
You need advanced flows like cancel via
iterator.return()
or custom schedulers (you can extendrun
to expose a cancel handle)