Skip to main content

07-09-async-await-pitfalls

Async/Await Pitfalls in TypeScript:

Handling Errors:

async functions make error handling cleaner with the use of try/catch blocks. Since these functions return promises, any error that occurs inside an async function results in a rejected promise. Use try/catch to handle these rejections and prevent unhandled promise rejections.

  • Code Example:

    async function fetchData() {
    try {
    let data = await someAsyncCall();
    return data;
    } catch (error) {
    console.error('An error occurred:', error);
    }
    }

Forgetting await:

When you forget to use await with a function that returns a promise, the code execution does not pause. This means the next lines of code will run before the promise settles, potentially leading to logic errors where you work with unresolved promises instead of their values.

  • Code Example:

    async function process() {
    let data = someAsyncFunction(); // Forgot to await
    console.log(data); // This logs a Promise, not the result
    }

Excessive Awaiting:

If you have multiple promises that are independent of each other, you don't need to wait for each one to resolve before starting the next one. Awaiting unnecessarily in sequence can slow down your application, instead run them in parallel.

  • Code Example:

    async function performTasks() {
    let result1 = await task1(); // These could be run in parallel
    let result2 = await task2(); // Instead of awaiting each one individually
    }

Overuse in Parallel Execution:

In a loop, if you use await for an operation that could be executed concurrently with others, you're not making full use of JavaScript's non-blocking nature. Instead, collect all the promises and then await them all with Promise.all.

  • Code Example:

    async function processItems(items) {
    let promises = items.map(item => asyncOperation(item));
    let results = await Promise.all(promises); // Run in parallel
    }

Error Handling in Loops:

When using loops with async/await, it’s important to handle errors in a way that one error does not stop the execution of the entire loop. You can handle this by putting a try/catch inside the loop's body.

  • Code Example:

    async function processArray(array) {
    for (const item of array) {
    try {
    await processItem(item);
    } catch (error) {
    console.error('Error processing item:', error);
    }
    }
    }

Mixing Thenables and Async/Await:

Mixing .then() and .catch() with async/await can lead to code that’s harder to understand and maintain. It's generally best to stick with one pattern for handling asynchronous operations consistently throughout your code.

  • Code Example:

    async function getData() {
    return fetchData()
    .then(data => process(data)) // Mixing thenables
    .catch(error => console.error(error)); // with async/await
    }
    // It's cleaner to use async/await exclusively

Ignoring Returned Promises:

An async function returns a promise, even if you don't explicitly return a value. Ignoring this promise means you're not handling the cases where it might reject due to an error, which can lead to unhandled promise rejections.

  • Code Example:

    async function runTask() {
    // This function returns a promise implicitly
    }

    runTask(); // Ignoring the promise here
    // Should handle the promise to catch any potential errors

Awaiting Non-Promises:

await is only useful for promises. If you use it with a non-promise value, TypeScript will convert that value to a resolved promise, which is an unnecessary step. While TypeScript allows this, it’s not good practice as it can create confusion.

  • Code Example:

    async function pointlessAwait() {
    let value = await 42; // 42 is not a promise
    // TypeScript turns it into a promise but it's unnecessary
    }
  1. Substitution Principle Violations:

    If you can't use a subclass wherever you use its superclass, it may violate the Liskov Substitution Principle. This means the subclass does not behave as expected when used in place of the superclass, leading to errors.

    • Code Example:

      class Bird {
      fly() {
      // implementation for flying
      }
      }

      class Penguin extends Bird {
      fly() {
      throw new Error("Cannot fly");
      }
      }

      // Using a Penguin in place of a Bird might cause issues if we expect to call 'fly' method.
  2. Increased Memory Overhead:

    Each object created from a subclass includes all the data from the superclass. If the subclass doesn't need all the properties of the superclass, this can waste memory.

    • Code Example:

      class Animal {
      // Assume lots of properties
      }

      class Dog extends Animal {
      // Dog might not need all Animal properties, leading to wasted memory.
      }
  3. Refactoring Challenges:

    Changing the structure of a base class is hard because it can affect every class that extends from it. This makes it difficult to make changes without breaking something.

    • Code Example:

      class Vehicle {
      // Many derived classes depend on Vehicle's implementation.
      }

      // Changing Vehicle can affect Car, Truck, Bike, etc.
      class Car extends Vehicle {}
  4. Single Responsibility Principle:

    Subclasses often do more than one thing, especially if they inherit from a base class that does a lot. This can violate the principle that a class should have only one reason to change.

    • Code Example:

      class User {
      // User class has multiple responsibilities
      }

      class AdminUser extends User {
      // Inherits all User responsibilities, possibly adding more.
      }
  5. Implicit Dependencies:

    When a class inherits from another, it might depend on the base class's behavior without that being clear in the code, making the codebase harder to understand.

    • Code Example:

      class Button {
      // Base class logic
      }

      class SaveButton extends Button {
      // Depends on the logic of Button in a way not obvious from the interface.
      }
  6. Base Class Over-generalization:

    Making a base class too general to cover every scenario makes it complicated and can lead to a lot of unused code.

    • Code Example:

      class Component {
      // Generic methods that might not be applicable to all subclasses
      }

      class Button extends Component {
      // Might not need everything from Component
      }
  7. Alternative Patterns:

    Using composition instead of inheritance can give you more control and flexibility. With composition, you build classes out of other classes without creating a strict parent-child relationship.

    • Code Example:

      class Engine {
      // Engine functionality
      }

      class Car {
      private engine: Engine;

      constructor(engine: Engine) {
      this.engine = engine;
      }

      // Car functionality using Engine without inheriting from it.
      }
  8. TypeScript Specific: Decorator Impact:

    Decorators enhance the behavior of classes, methods, or properties in TypeScript. However, they don't automatically apply to subclasses, which can lead to unexpected behaviors if not planned for.

    • Code Example:

      function Component(constructor: Function) {
      // Decorator logic
      }

      @Component
      class Control {
      // Some behavior added by the decorator
      }

      class Button extends Control {
      // Does not inherit the decorator-applied behaviors
      }