[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 extend run to expose a cancel handle)

posted @ 2025-08-09 22:29  Zhentiw  阅读(8)  评论(0)    收藏  举报