JavaScript
Promises
Asynchronous Programming
Synchronous Code
Async/Await Alternative

Promises - How to make asynchronous code execute synchronous without async / await?

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

In JavaScript, asynchronous operations cannot be made truly synchronous without blocking the event loop, and blocking is usually the wrong choice for application code. The practical goal is sequential execution with clear control flow. You can achieve that with promise chaining, even when async and await are not used.

Run Tasks Sequentially with Promise Chains

A common pattern is to reduce an array of steps into a single promise chain. Each step starts only after the previous one resolves.

javascript
1function wait(ms) {
2  return new Promise(resolve => setTimeout(resolve, ms));
3}
4
5function step(label, ms) {
6  return wait(ms).then(() => {
7    console.log(`finished: ${label}`);
8    return label;
9  });
10}
11
12const jobs = [
13  () => step("download", 300),
14  () => step("transform", 200),
15  () => step("upload", 250),
16];
17
18jobs.reduce((chain, job) => chain.then(job), Promise.resolve())
19  .then(() => console.log("all steps completed"))
20  .catch(err => console.error("pipeline failed", err));

This gives sequential behavior without pretending the code is synchronous.

Sequence Dynamic Workloads

If the number of tasks is dynamic, wrap sequencing in a helper. This keeps call sites clean and easy to test.

javascript
1function runSequentially(taskFactories) {
2  const results = [];
3
4  return taskFactories.reduce((p, createTask) => {
5    return p.then(() => createTask()).then(value => {
6      results.push(value);
7    });
8  }, Promise.resolve()).then(() => results);
9}
10
11runSequentially([
12  () => Promise.resolve(1),
13  () => Promise.resolve(2),
14  () => Promise.resolve(3),
15]).then(values => console.log(values));

A helper like this makes ordering guarantees explicit in your API.

Error Handling and Cleanup

Promise chains should include centralized error handling and optional cleanup steps. If one step fails, later steps should not run unless you intentionally recover.

javascript
1function runWithCleanup(tasks, cleanup) {
2  return tasks
3    .reduce((chain, task) => chain.then(task), Promise.resolve())
4    .catch(err => {
5      console.error("task failed", err);
6      throw err;
7    })
8    .finally(() => cleanup());
9}

This structure keeps failure behavior predictable in production workflows.

Refactor Callback Workflows into Promise Pipelines

Many requests to make async code synchronous come from callback-heavy legacy code. The practical refactor is to wrap callback APIs in promises and then chain them sequentially. This improves readability and error propagation without changing runtime semantics.

javascript
1function readConfig(cb) {
2  setTimeout(() => cb(null, { retries: 2 }), 100);
3}
4
5function fetchRemote(config, cb) {
6  setTimeout(() => cb(null, `data-with-${config.retries}`), 100);
7}
8
9function saveResult(data, cb) {
10  setTimeout(() => cb(null, `saved:${data}`), 100);
11}
12
13function toPromise(fn, ...args) {
14  return new Promise((resolve, reject) => {
15    fn(...args, (err, value) => {
16      if (err) reject(err);
17      else resolve(value);
18    });
19  });
20}
21
22toPromise(readConfig)
23  .then(config => toPromise(fetchRemote, config))
24  .then(data => toPromise(saveResult, data))
25  .then(result => console.log(result))
26  .catch(err => console.error(err));

This pipeline runs in order, surfaces failures correctly, and avoids nested callback pyramids. It also creates a clear path to adopting async and await later if your style guide allows it.

Use Queues for User-Triggered Sequential Tasks

When users trigger actions rapidly, queue tasks instead of starting all operations at once. A promise queue guarantees order and prevents resource contention in clients and services.

javascript
1let queue = Promise.resolve();
2
3function enqueue(taskFactory) {
4  queue = queue.then(taskFactory).catch(err => {
5    console.error("queued task failed", err);
6  });
7  return queue;
8}
9
10enqueue(() => step("A", 100));
11enqueue(() => step("B", 100));
12enqueue(() => step("C", 100));

This pattern gives synchronous-like sequencing while preserving non-blocking execution.

Prefer one sequencing style per module so asynchronous control flow stays easy to read.

Common Pitfalls

  • Trying to block the event loop to force sync behavior.
  • Launching all promises with Promise.all when order is required.
  • Forgetting to return inner promises in .then callbacks.
  • Mixing callbacks and promises in ways that hide errors.

Summary

  • JavaScript cannot safely make async I O truly synchronous in normal app code.
  • Promise chains provide deterministic sequential execution.
  • Encapsulate chaining in helpers for reuse and testing.
  • Centralize error handling and cleanup for reliable pipelines.

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.