Practice Questions on Handling Errors in Asynchronous Code in Dart

  1. Create a function that simulates fetching user data. If the user ID is invalid, throw an exception. Handle the exception using a try/catch block when calling the function.
  2. Write a program that demonstrates using catchError with a Future that may fail. If the Future fails, print an error message.
  3. Implement a function that returns a Future which throws an exception after a delay. Use async and await to call this function, and handle the error appropriately.
  4. Create a stream that yields numbers from 1 to 5, but throws an error when it reaches 3. Use the onError callback to handle the error when listening to the stream.
  5. Write a function that takes a nullable integer and returns its reciprocal. If the integer is null or zero, throw an exception. Handle the exception using a try/catch block in the main function.
  6. Implement a function that simulates a network request using a Future that either resolves with data or throws an error. Use then and catchError to handle the result and error.
  7. Create a class that manages a list of items. Include a method to remove an item by index. If the index is out of bounds, throw an exception. Handle the exception when calling this method.
  8. Write a program that demonstrates global error handling using runZonedGuarded. Create a function that throws an exception, and catch the error in the zone.
  9. Implement a method that reads data from a file asynchronously. If the file does not exist, throw an exception. Handle the exception using a try/catch block.
  10. Create a function that fetches data from an API. If the API call fails, handle the error using both try/catch and catchError to show different handling approaches.

Solutions and Explanations

  1. Fetching User Data

    Future<String> fetchUserData(int userId) async {
      if (userId <= 0) {
        throw Exception('Invalid user ID');
      }
      return 'User data for ID: $userId';
    }
    
    void main() async {
      try {
        String data = await fetchUserData(-1);
        print(data);
      } catch (e) {
        print('Caught error: $e'); // Output: Caught error: Exception: Invalid user ID   }
    }
    

    Explanation: The fetchUserData function checks if the user ID is valid. If not, it throws an exception. In the main function, we use try/catch to handle the exception.

  2. Using catchError with a Future

    Future<String> riskyOperation() {
      return Future.delayed(Duration(seconds: 1), () {
        throw Exception('Operation failed');
      });
    }
    
    void main() {
      riskyOperation().catchError((error) {
        print('Caught error: $error'); // Output: Caught error: Exception: Operation failed   });
    }
    

    Explanation: The riskyOperation function simulates an operation that fails. In main, we use catchError to handle the error without needing a try/catch.

  3. Async Function with Exception

    Future<void> throwError() async {
      await Future.delayed(Duration(seconds: 1));
      throw Exception('An error occurred');
    }
    
    void main() async {
      try {
        await throwError();
      } catch (e) {
        print('Caught error: $e'); // Output: Caught error: Exception: An error occurred   }
    }
    

    Explanation: The throwError function simulates a delayed error. We await its completion in main, catching any exceptions thrown.

  4. Stream with Error Handling

    Stream<int> numberStream() async* {
      for (int i = 1; i <= 5; i++) {
        await Future.delayed(Duration(seconds: 1));
        if (i == 3) {
          throw Exception('Error at number 3');
        }
        yield i;
      }
    }
    
    void main() {
      numberStream().listen(
        (number) => print(number),
        onError: (error) => print('Caught error: $error'), // Output: Caught error: Exception: Error at number 3  
         );
    }
    

    Explanation: The numberStream yields numbers from 1 to 5 but throws an error at 3. The onError callback in listen handles the exception.

  5. Nullable Integer Reciprocal

    double reciprocal(int? number) {
      if (number == null || number == 0) {
        throw Exception('Cannot calculate reciprocal of null or zero');
      }
      return 1 / number;
    }
    
    void main() {
      try {
        print(reciprocal(null));
      } catch (e) {
        print('Caught error: $e'); // Output: Caught error: Exception: Cannot calculate reciprocal of null or zero   
        }
    }
    

    Explanation: The reciprocal function checks if the input is null or zero and throws an exception. The error is caught in main.

  6. Simulating a Network Request

    Future<String> fetchFromNetwork() {
      return Future.delayed(Duration(seconds: 2), () {
        throw Exception('Network error');
      });
    }
    
    void main() {
      fetchFromNetwork().then((data) {
        print(data);
      }).catchError((error) {
        print('Caught error: $error'); // Output: Caught error: Exception: Network error   
        });
    }
    

    Explanation: The fetchFromNetwork function simulates a network request that fails. We handle the error using then and catchError.

  7. Managing a List of Items

    class ItemManager {
      List<String> items = [];
    
      void removeItem(int index) {
        if (index < 0 || index >= items.length) {
          throw Exception('Index out of bounds');
        }
        items.removeAt(index);
      }
    }
    
    void main() {
      ItemManager manager = ItemManager();
      try {
        manager.removeItem(0); // This will throw an error   
        } catch (e) {
        print('Caught error: $e'); // Output: Caught error: Exception: Index out of bounds   
        }
    }
    

    Explanation: The ItemManager class has a method to remove an item by index. If the index is invalid, it throws an exception, which is caught in main.

  8. Global Error Handling with Zones

    void throwError() {
      throw Exception('An exception occurred in the zone');
    }
    
    void main() {
      runZonedGuarded(() {
        throwError();
      }, (error, stackTrace) {
        print('Caught error in zone: $error'); // Output: Caught error in zone: Exception: An exception occurred in the zone  
         });
    }
    

    Explanation: In this example, runZonedGuarded captures any uncaught exceptions thrown within the zone, allowing for centralized error handling.

  9. Reading Data from a File

    import 'dart:io';
    
    Future<String> readFile(String path) async {
      if (!await File(path).exists()) {
        throw Exception('File not found');
      }
      return await File(path).readAsString();
    }
    
    void main() async {
      try {
        String data = await readFile('path/to/file.txt');
        print(data);
      } catch (e) {
        print('Caught error: $e'); // Output: Caught error: Exception: File not found   
        }
    }
    

    Explanation: The readFile function checks if a file exists and throws an exception if it does not. This is handled in main using a try/catch block.

  10. Fetching Data from an API

    Future<String> fetchDataFromApi() {
      return Future.delayed(Duration(seconds: 1), () {
        throw Exception('API call failed');
      });
    }
    
    void main() async {
      try {
        String data = await fetchDataFromApi();
        print(data);
      } catch (e) {
        print('Caught error using try/catch: $e'); // Output: Caught error using try/catch: Exception: API call failed   }
    
      fetchDataFromApi().catchError((e) {
        print('Caught error using catchError: $e'); // Output: Caught error using catchError: Exception: API call failed   });
    }
    

    Explanation: The fetchDataFromApi function simulates an API call that fails. We demonstrate error handling using both try/catch and catchError to show different approaches.