🔥 0
0
Lesson 7 of 10 15 min +85 XP

Async/Await

Callbacks work, but they can get messy. Promises and async/await give you cleaner asynchronous code.

The Callback Problem

// Callback hell (pyramid of doom)
fs.readFile('file1.txt', 'utf8', (err, data1) => {
  if (err) return console.error(err);
  fs.readFile('file2.txt', 'utf8', (err, data2) => {
    if (err) return console.error(err);
    fs.readFile('file3.txt', 'utf8', (err, data3) => {
      if (err) return console.error(err);
      console.log(data1, data2, data3);
    });
  });
});

Hard to read, hard to maintain. Let's fix this.

Promises Basics

A Promise represents a value that will be available later:

// A Promise has three states:
// - Pending: still working
// - Fulfilled: completed successfully
// - Rejected: failed with an error

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    const success = true;
    if (success) {
      resolve('It worked!');
    } else {
      reject(new Error('Something failed'));
    }
  }, 1000);
});

// Using the promise
promise
  .then(result => console.log(result))  // 'It worked!'
  .catch(error => console.error(error));

fs with Promises

Node.js has promise-based versions of fs functions:

// Using fs/promises (Node.js 14+)
const fs = require('fs').promises;

fs.readFile('hello.txt', 'utf8')
  .then(data => {
    console.log('File contents:', data);
  })
  .catch(err => {
    console.error('Error:', err.message);
  });

Enter async/await

async/await is syntactic sugar for Promises. Same thing, cleaner syntax:

// async-read.js
const fs = require('fs').promises;

async function readMyFile() {
  try {
    const data = await fs.readFile('hello.txt', 'utf8');
    console.log('File contents:', data);
  } catch (err) {
    console.error('Error:', err.message);
  }
}

readMyFile();
Key points:
  • async marks a function as asynchronous
  • await pauses until the Promise resolves
  • Use try/catch for error handling

Before and After

Callbacks:
fs.readFile('config.json', 'utf8', (err, data) => {
  if (err) {
    console.error(err);
    return;
  }
  const config = JSON.parse(data);
  console.log(config.name);
});
async/await:
async function loadConfig() {
  try {
    const data = await fs.readFile('config.json', 'utf8');
    const config = JSON.parse(data);
    console.log(config.name);
  } catch (err) {
    console.error(err);
  }
}

The async version reads like synchronous code but doesn't block!

Sequential vs Parallel

Sequential (one after another):
async function sequential() {
  const file1 = await fs.readFile('a.txt', 'utf8');
  const file2 = await fs.readFile('b.txt', 'utf8');
  const file3 = await fs.readFile('c.txt', 'utf8');
  // Total time: file1 + file2 + file3
  return [file1, file2, file3];
}
Parallel (all at once):
async function parallel() {
  const [file1, file2, file3] = await Promise.all([
    fs.readFile('a.txt', 'utf8'),
    fs.readFile('b.txt', 'utf8'),
    fs.readFile('c.txt', 'utf8')
  ]);
  // Total time: max(file1, file2, file3)
  return [file1, file2, file3];
}

Use Promise.all when operations don't depend on each other!

Real Example: Fetch User Data

// user-service.js

// Simulate async database calls
function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function getUser(id) {
  await delay(100); // Simulate DB lookup
  return { id, name: 'Alice', email: 'alice@example.com' };
}

async function getPosts(userId) {
  await delay(100);
  return [
    { id: 1, title: 'First Post' },
    { id: 2, title: 'Second Post' }
  ];
}

async function getComments(postId) {
  await delay(50);
  return [
    { id: 1, text: 'Great post!' },
    { id: 2, text: 'Thanks for sharing' }
  ];
}

// Combine them all
async function getUserDashboard(userId) {
  try {
    // Get user first
    const user = await getUser(userId);
    console.log('Got user:', user.name);

    // Get posts (depends on user)
    const posts = await getPosts(user.id);
    console.log('Got', posts.length, 'posts');

    // Get comments for all posts in parallel
    const allComments = await Promise.all(
      posts.map(post => getComments(post.id))
    );
    console.log('Got comments for all posts');

    return {
      user,
      posts: posts.map((post, i) => ({
        ...post,
        comments: allComments[i]
      }))
    };
  } catch (err) {
    console.error('Dashboard error:', err.message);
    throw err;
  }
}

// Run it
getUserDashboard(1).then(dashboard => {
  console.log('\nDashboard:', JSON.stringify(dashboard, null, 2));
});
Output:
Got user: Alice
Got 2 posts
Got comments for all posts

Dashboard: {
  "user": { "id": 1, "name": "Alice", "email": "alice@example.com" },
  "posts": [
    {
      "id": 1,
      "title": "First Post",
      "comments": [...]
    },
    ...
  ]
}

Error Handling Patterns

Try/catch block:
async function fetchData() {
  try {
    const data = await riskyOperation();
    return data;
  } catch (err) {
    console.error('Operation failed:', err.message);
    return null; // or throw err to propagate
  }
}
Handle specific errors:
async function readConfig() {
  try {
    const data = await fs.readFile('config.json', 'utf8');
    return JSON.parse(data);
  } catch (err) {
    if (err.code === 'ENOENT') {
      console.log('Config not found, using defaults');
      return { port: 3000 };
    }
    throw err; // Re-throw other errors
  }
}
Crio.Do

Practice Async Patterns

Build real Node.js applications with async/await on cloud workspaces. Pre-configured environments with databases included.

Start Free Trial

Try It Yourself

Create a file processor that reads, transforms, and writes data:

// processor.js
const fs = require('fs').promises;

async function processFile(inputPath, outputPath) {
  try {
    // Read input
    console.log('Reading:', inputPath);
    const input = await fs.readFile(inputPath, 'utf8');

    // Transform (uppercase and add line numbers)
    const lines = input.split('\n');
    const output = lines
      .map((line, i) => `${i + 1}: ${line.toUpperCase()}`)
      .join('\n');

    // Write output
    console.log('Writing:', outputPath);
    await fs.writeFile(outputPath, output);

    console.log('Done! Processed', lines.length, 'lines');
  } catch (err) {
    if (err.code === 'ENOENT') {
      console.error('File not found:', inputPath);
    } else {
      console.error('Error:', err.message);
    }
  }
}

// Run it
const input = process.argv[2] || 'input.txt';
const output = process.argv[3] || 'output.txt';
processFile(input, output);
Run it:
echo "hello world\nthis is a test" > input.txt
node processor.js input.txt result.txt
cat result.txt
Output:
Reading: input.txt
Writing: result.txt
Done! Processed 2 lines
result.txt:
1: HELLO WORLD
2: THIS IS A TEST

Key Takeaways

  • Promises represent future values
  • async marks functions that return Promises
  • await pauses until a Promise resolves
  • try/catch handles errors in async functions
  • Promise.all runs multiple operations in parallel
  • fs.promises gives you promise-based file operations

Next, let's build an HTTP server!

🧠 Quick Quiz

Test your understanding of this lesson.

1

What does the 'await' keyword do?

2

Where can you use the 'await' keyword?

3

How do you handle errors with async/await?

Working with Files