Skip to content

Intégration de try-catch avec RxJS ​

JavaScript dispose de try-catch comme gestion standard des erreurs, mais RxJS a ses propres mécanismes de gestion des erreurs comme catchError et subscribe.error. Cette page explique les différences entre les deux et les méthodes appropriées pour les combiner.

try-catch vs Gestion des erreurs RxJS ​

Différences fondamentales ​

Élémenttry-catchcatchError / subscribe.error
CibleCode synchroneStreams asynchrones
Type d'erreursExceptions JavaScriptToutes les erreurs dans l'Observable
PortéeUniquement dans le bloc try-catchTout le pipeline
Support asynchrone❌ Ne peut pas capturer les erreurs asynchrones✅ Peut capturer les erreurs asynchrones
Continuité du stream-✅ Peut continuer avec catchError

Erreurs que try-catch ne peut pas capturer ​

Les erreurs asynchrones suivantes ne peuvent pas être capturées par try-catch.

typescript
import { timer, throwError, mergeMap, catchError } from 'rxjs';
// ❌ try-catch ne peut pas capturer les erreurs asynchrones
try {
  setTimeout(() => {
    throw new Error('Erreur asynchrone');
  }, 1000);
} catch (error) {
  // Ceci ne sera pas exécuté
  console.error('Capturé:', error);
}

// ✅ RxJS peut capturer les erreurs asynchrones

timer(1000).pipe(
  mergeMap(() => throwError(() => new Error('Erreur asynchrone'))),
  catchError((error: unknown) => {
    console.error('Capturé:', (error instanceof Error ? error.message : String(error))); // ✅ Exécuté
    return of('Valeur par défaut');
  })
).subscribe();

Limitation de try-catch

try-catch cible uniquement le code synchrone. Il ne peut pas capturer les erreurs survenant dans les traitements asynchrones comme setTimeout, requêtes HTTP, Promise, Observable.

Cas où utiliser try-catch ​

Même en utilisant RxJS, try-catch est nécessaire dans les cas suivants.

1. Gestion des erreurs lors du nettoyage des ressources ​

Dans finalize ou using, utilisez try-catch pour capturer les erreurs lors de la libération des ressources.

typescript
import { interval, take, finalize } from 'rxjs';
let ws: WebSocket | null = null;

interval(1000).pipe(
  take(5),
  finalize(() => {
    // Traiter les erreurs de libération des ressources avec try-catch
    try {
      if (ws) {
        ws.close();
        console.log('WebSocket fermé normalement');
      }
    } catch (error) {
      // L'application continue même si la fermeture échoue
      console.error('Erreur de fermeture WebSocket:', error);
    }
  })
).subscribe({
  next: val => console.log('Valeur:', val)
});

Raison: La libération des ressources peut échouer mais on veut souvent continuer l'application.

2. Gestion des erreurs synchrones dans les Observable personnalisés ​

Lors de la conversion d'erreurs synchrones en erreurs RxJS dans new Observable().

typescript
import { Observable } from 'rxjs';

interface User {
  id: number;
  name: string;
}

function parseUserData(json: string): Observable<User> {
  return new Observable<User>(subscriber => {
    try {
      // JSON.parse est synchrone donc capturer avec try-catch
      const data = JSON.parse(json);

      // Validation
      if (!data.id || !data.name) {
        throw new Error('Données utilisateur invalides');
      }

      subscriber.next(data);
      subscriber.complete();
    } catch (error) {
      // Convertir l'erreur synchrone en erreur RxJS
      subscriber.error(error);
    }
  });
}

// Exemple d'utilisation
parseUserData('{"id": 1, "name": "Taro"}').subscribe({
  next: user => console.log('Utilisateur:', user),
  error: err => console.error('Erreur:', err.message)
});

// JSON invalide
parseUserData('invalid json').subscribe({
  next: user => console.log('Utilisateur:', user),
  error: err => console.error('Erreur:', err.message) // Capturer l'erreur JSON.parse
});

Raison: Pour propager les erreurs de traitements synchrones (JSON.parse, validation, etc.) comme erreurs Observable.

3. Intégration avec des bibliothèques externes ​

Gestion des erreurs lors de l'utilisation de bibliothèques externes qui n'utilisent pas RxJS.

typescript
import { of, map, catchError } from 'rxjs';
// Bibliothèque externe (ex: bibliothèque de traitement d'images)
declare const ExternalImageLib: {
  process(data: string): string;
};

of('image-data-1', 'image-data-2', 'invalid-data').pipe(
  map(imageData => {
    try {
      // La bibliothèque externe peut lever des exceptions
      const processed = ExternalImageLib.process(imageData);
      return { success: true, data: processed };
    } catch (error) {
      // Capturer l'erreur synchrone et retourner comme valeur avec info d'erreur
      console.error('Erreur de traitement d\'image:', error);
      return { success: false, error: (error as Error).message };
    }
  })
).subscribe({
  next: result => {
    if (result.success) {
      console.log('Traitement réussi:', result.data);
    } else {
      console.log('Traitement échoué:', result.error);
    }
  }
});

Raison: Pour traiter les erreurs des bibliothèques externes en toute sécurité dans le stream et continuer le stream.

4. Traitement synchrone dans les opérateurs ​

Lors de traitements synchrones dans des opérateurs comme map, tap.

typescript
import { of, map } from 'rxjs';
interface Config {
  apiUrl: string;
  timeout: number;
}

of('{"apiUrl": "https://api.example.com", "timeout": 5000}').pipe(
  map(jsonString => {
    try {
      const config: Config = JSON.parse(jsonString);

      // Validation des valeurs de configuration
      if (config.timeout < 0) {
        throw new Error('Le timeout doit être un nombre positif');
      }

      return config;
    } catch (error) {
      console.error('Erreur d\'analyse de la configuration:', error);
      // Retourner une configuration par défaut
      return { apiUrl: 'https://default.example.com', timeout: 3000 };
    }
  })
).subscribe({
  next: config => console.log('Configuration:', config)
});

Raison: Pour traiter les erreurs synchrones dans les opérateurs et retourner des valeurs par défaut sans interrompre le stream.

Patterns pratiques de combinaison ​

Pattern 1: Gestion hiérarchique des erreurs ​

Approche hiérarchique où les erreurs synchrones sont traitées avec try-catch et les erreurs asynchrones avec catchError.

typescript
import { of, map, catchError, finalize } from 'rxjs';
import { ajax } from 'rxjs/ajax';

interface ApiResponse {
  data: string;
  timestamp: number;
}

let connection: any = null;

ajax.getJSON<ApiResponse>('https://api.example.com/data').pipe(
  // Niveau 1: Traiter les erreurs de traitement synchrone avec try-catch
  map(response => {
    try {
      // Transformation des données (traitement synchrone)
      const decoded = atob(response.data);
      return { decoded, timestamp: response.timestamp };
    } catch (error) {
      console.error('Erreur de décodage:', error);
      throw new Error('Échec de la transformation des données');
    }
  }),
  // Niveau 2: Traiter les erreurs asynchrones avec catchError
  catchError((error: unknown) => {
    console.error('Erreur d\'appel API:', error);
    return of({ decoded: '', timestamp: Date.now() });
  }),
  // Niveau 3: Protéger le nettoyage des ressources avec try-catch
  finalize(() => {
    try {
      if (connection) {
        connection.close();
        console.log('Connexion fermée');
      }
    } catch (error) {
      console.error('Erreur de fermeture:', error);
    }
  })
).subscribe({
  next: result => console.log('Résultat:', result),
  error: err => console.error('Erreur finale:', err)
});

Pattern 2: Gestion des ressources avec using() ​

typescript
import { using, interval, take } from 'rxjs';
interface ManagedResource {
  id: string;
  close: () => void;
}

function createManagedStream(resource: ManagedResource) {
  return using(
    // Factory de ressources
    () => {
      console.log('Création de ressource:', resource.id);

      return {
        unsubscribe: () => {
          // Traiter les erreurs de libération de ressources avec try-catch
          try {
            resource.close();
            console.log('Libération de ressource réussie:', resource.id);
          } catch (error) {
            console.error('Erreur de libération de ressource:', resource.id, error);
            // L'application continue même si une erreur se produit
          }
        }
      };
    },
    // Factory d'Observable
    () => interval(1000).pipe(take(3))
  );
}

// Exemple d'utilisation
const resource: ManagedResource = {
  id: 'resource-1',
  close: () => {
    // La fermeture peut échouer
    if (Math.random() > 0.5) {
      throw new Error('Échec de fermeture');
    }
  }
};

createManagedStream(resource).subscribe({
  next: val => console.log('Valeur:', val),
  complete: () => console.log('Terminé')
});

Pattern 3: Transformation d'erreurs personnalisée ​

Transformer les erreurs d'API externes en erreurs spécifiques à l'application.

typescript
import { Observable, throwError, catchError, map } from 'rxjs';
import { ajax } from 'rxjs/ajax';

// Classes d'erreur personnalisées
class ValidationError extends Error {
  constructor(message: string, public field: string) {
    super(message);
    this.name = 'ValidationError';
  }
}

class NetworkError extends Error {
  constructor(message: string, public statusCode: number) {
    super(message);
    this.name = 'NetworkError';
  }
}

interface UserData {
  email: string;
  age: number;
}

function validateAndFetchUser(userId: string): Observable<UserData> {
  return ajax.getJSON<UserData>(`https://api.example.com/users/${userId}`).pipe(
    map(user => {
      try {
        // Validation synchrone
        if (!user.email || !user.email.includes('@')) {
          throw new ValidationError('Adresse email invalide', 'email');
        }
        if (user.age < 0 || user.age > 150) {
          throw new ValidationError('Âge invalide', 'age');
        }
        return user;
      } catch (error) {
        if (error instanceof ValidationError) {
          // Re-throw les erreurs de validation comme erreurs RxJS
          throw error;
        }
        throw new Error('Erreur de validation des données');
      }
    }),
    catchError((error: unknown) => {
      // Traiter les erreurs asynchrones (erreurs HTTP, etc.)
      if (error.status) {
        const networkError = new NetworkError(
          `HTTP ${error.status}: ${(error instanceof Error ? error.message : String(error))}`,
          error.status
        );
        return throwError(() => networkError);
      }
      return throwError(() => error);
    })
  );
}

// Exemple d'utilisation
validateAndFetchUser('123').subscribe({
  next: user => console.log('Utilisateur:', user),
  error: err => {
    if (err instanceof ValidationError) {
      console.error(`Erreur de validation (${err.field}):`, err.message);
    } else if (err instanceof NetworkError) {
      console.error(`Erreur réseau (${err.statusCode}):`, err.message);
    } else {
      console.error('Erreur inattendue:', err);
    }
  }
});

Diagramme de séquence: Coopération entre try-catch et catchError ​

Comme le montre ce diagramme ​

  • Erreur synchrone: Capturée par try-catch → Traitée → Stream continue
  • Erreur asynchrone: Contourne try-catch → Capturée par catchError

Anti-patterns ​

❌ Anti-pattern 1: Essayer de capturer les erreurs asynchrones avec try-catch ​

typescript
// ❌ Mauvais exemple: Les erreurs asynchrones ne peuvent pas être capturées
import { ajax } from 'rxjs/ajax';

try {
  ajax.getJSON('https://api.example.com/data').subscribe({
    next: data => console.log(data)
  });
} catch (error) {
  // Les erreurs HTTP ne sont pas capturées ici
  console.error('Erreur:', error);
}
typescript
import { of, catchError } from 'rxjs';
// ✅ Bon exemple: Traiter avec catchError ou subscribe.error
import { ajax } from 'rxjs/ajax';

ajax.getJSON('https://api.example.com/data').pipe(
  catchError((error: unknown) => {
    console.error('Erreur HTTP:', error);
    return of(null);
  })
).subscribe({
  next: data => console.log(data)
});

❌ Anti-pattern 2: Étouffer les erreurs avec try-catch ​

typescript
import { of, map } from 'rxjs';
// ❌ Mauvais exemple: Juste logger l'erreur et l'étouffer

of('invalid-json').pipe(
  map(json => {
    try {
      return JSON.parse(json);
    } catch (error) {
      console.error('Erreur:', error);
      return null; // L'information d'erreur est perdue
    }
  })
).subscribe({
  next: data => {
    // On ne remarque pas si data est null
    console.log(data);
  }
});
typescript
import { of, map } from 'rxjs';
// ✅ Bon exemple: Préserver l'information d'erreur

interface ParseResult {
  success: boolean;
  data?: any;
  error?: string;
}

of('invalid-json').pipe(
  map(json => {
    try {
      const data = JSON.parse(json);
      return { success: true, data } as ParseResult;
    } catch (error) {
      return {
        success: false,
        error: (error as Error).message
      } as ParseResult;
    }
  })
).subscribe({
  next: result => {
    if (result.success) {
      console.log('Données:', result.data);
    } else {
      console.error('Erreur de parsing:', result.error);
    }
  }
});

❌ Anti-pattern 3: Utilisation excessive de try-catch ​

typescript
import { of, map } from 'rxjs';
// ❌ Mauvais exemple: Envelopper avec try-catch des erreurs que RxJS peut traiter

of(1, 2, 3).pipe(
  map(x => {
    try {
      return x * 10;
    } catch (error) {
      // Ce code ne lève pas d'erreur donc inutile
      return 0;
    }
  })
).subscribe();
typescript
import { of, map } from 'rxjs';
// ✅ Bon exemple: Utiliser try-catch seulement où nécessaire

of('{"value": 1}', 'invalid', '{"value": 2}').pipe(
  map(json => {
    try {
      // JSON.parse peut lever une exception donc try-catch est nécessaire
      return JSON.parse(json);
    } catch (error) {
      console.error('Erreur d\'analyse JSON:', error);
      return { value: 0 };
    }
  })
).subscribe({
  next: data => console.log('Données:', data)
});

Bonnes pratiques ​

1. Différencier selon le type d'erreur ​

typescript
import { of, map, catchError, finalize } from 'rxjs';
import { ajax } from 'rxjs/ajax';

// Différenciation claire
ajax.getJSON<{ data: string }>('https://api.example.com/data').pipe(
  // Erreurs de traitement synchrone → try-catch
  map(response => {
    try {
      return JSON.parse(response.data);
    } catch (error) {
      console.error('Erreur de parsing:', error);
      return {};
    }
  }),
  // Erreurs asynchrones → catchError
  catchError((error: unknown) => {
    console.error('Erreur API:', error);
    return of({});
  }),
  // Erreurs lors du nettoyage → try-catch
  finalize(() => {
    try {
      // Traitement de libération des ressources
    } catch (error) {
      console.error('Erreur de nettoyage:', error);
    }
  })
).subscribe();

2. Préserver l'information d'erreur ​

typescript
import { of, map } from 'rxjs';
interface Result<T> {
  success: boolean;
  value?: T;
  error?: string;
}

function safeParse<T>(json: string): Result<T> {
  try {
    const value = JSON.parse(json);
    return { success: true, value };
  } catch (error) {
    return {
      success: false,
      error: error instanceof Error ? error.message : String(error)
    };
  }
}

of('{"name": "Taro"}', 'invalid').pipe(
  map(json => safeParse(json))
).subscribe({
  next: result => {
    if (result.success) {
      console.log('Succès:', result.value);
    } else {
      console.error('Échec:', result.error);
    }
  }
});

3. Fournir des informations détaillées avec des erreurs personnalisées ​

typescript
import { of, map } from 'rxjs';
// Classe d'erreur personnalisée
class DataProcessingError extends Error {
  constructor(
    message: string,
    public readonly data: unknown,
    public readonly step: string
  ) {
    super(message);
    this.name = 'DataProcessingError';
  }
}


of({ raw: 'some-data' }).pipe(
  map(input => {
    try {
      // Traitement complexe
      const processed = processData(input.raw);
      return processed;
    } catch (error) {
      throw new DataProcessingError(
        'Échec du traitement des données',
        input,
        'processing'
      );
    }
  })
).subscribe({
  next: data => console.log('Traité:', data),
  error: (err: DataProcessingError) => {
    console.error(`Erreur (${err.step}):`, err.message);
    console.error('Données d\'entrée:', err.data);
  }
});

function processData(raw: string): any {
  return raw;
}

4. Journalisation et monitoring ​

typescript
import { of, map, catchError, tap } from 'rxjs';
import { ajax } from 'rxjs/ajax';

// Fonction de surveillance des erreurs
function logError(context: string, error: Error): void {
  console.error(`[${context}]`, error.message);
  // En production, envoyer au service de surveillance des erreurs
  // errorMonitoringService.captureException(error, { context });
}

ajax.getJSON('https://api.example.com/data').pipe(
  tap(data => console.log('Reçu:', data)),
  map(data => {
    try {
      // Transformation des données
      return transformData(data);
    } catch (error) {
      logError('Data Transformation', error as Error);
      throw error; // Propager à catchError
    }
  }),
  catchError((error: unknown) => {
    logError('API Request', error);
    return of(null);
  })
).subscribe();

function transformData(data: any): any {
  return data;
}

Questions fréquentes ​

Q: Faut-il utiliser à la fois try-catch et catchError?

R: Oui, une différenciation est nécessaire.

  • try-catch: Erreurs de traitement synchrone (JSON.parse, validation, etc.)
  • catchError: Erreurs asynchrones (HTTP, timers, Promise, etc.)

Q: Est-il préférable de throw dans map ou de traiter avec try-catch?

R: Cela dépend de la situation.

  • throw: Quand vous voulez propager l'erreur à tout le stream
  • Traiter avec try-catch: Quand vous voulez récupérer sur place et continuer le stream
typescript
// Exemple de throw
map(x => {
  if (x < 0) throw new Error('Nombre négatif');
  return x;
})

// Exemple de traitement avec try-catch
map(x => {
  try {
    return riskyOperation(x);
  } catch (error) {
    return defaultValue; // Récupération sur place
  }
})

Q: Que se passe-t-il si une erreur se produit dans finalize?

R: Si une erreur se produit dans finalize, elle n'est pas propagée à subscribe.error, mais affichée comme Uncaught Error dans la console. Par conséquent, vous devez toujours capturer les erreurs avec try-catch dans finalize.

Q: Peut-on utiliser la combinaison async/await et try-catch?

R: Oui, c'est efficace pour gérer les Promise.

typescript
import { from, mergeMap } from 'rxjs';
async function fetchData(id: string): Promise<any> {
  try {
    const response = await fetch(`/api/data/${id}`);
    return await response.json();
  } catch (error) {
    console.error('Erreur Fetch:', error);
    throw error;
  }
}

from([1, 2, 3]).pipe(
  mergeMap(id => from(fetchData(String(id))))
).subscribe({
  next: data => console.log('Données:', data),
  error: err => console.error('Erreur:', err)
});

Q: Dans un Observable personnalisé, faut-il appeler subscriber.error() ou throw?

R: Dans new Observable(), utilisez subscriber.error().

typescript
// ✅ Correct
new Observable(subscriber => {
  try {
    const data = riskyOperation();
    subscriber.next(data);
  } catch (error) {
    subscriber.error(error); // Propager comme erreur RxJS
  }
})

// ❌ Incorrect (throw dans le constructeur Observable peut causer un comportement inattendu)
new Observable(subscriber => {
  const data = riskyOperation(); // L'erreur peut ne pas être capturée
  subscriber.next(data);
})

Résumé ​

try-catch et la gestion des erreurs RxJS ont chacun des rôles différents, et en les combinant, une gestion robuste des erreurs peut être réalisée.

Principes de différenciation ​

ScénarioRecommandéRaison
Traitement synchrone (JSON.parse, validation)try-catchCapturer immédiatement les erreurs synchrones
Traitement asynchrone (HTTP, timers)catchErrorCapturer les erreurs asynchrones
Libération des ressources (finalize, using)try-catchTraiter en toute sécurité les échecs de libération
À l'intérieur d'un Observable personnalisétry-catch + subscriber.error()Convertir les erreurs synchrones en erreurs RxJS
Intégration avec bibliothèques externestry-catchTraiter en toute sécurité les erreurs des bibliothèques

Principes importants

  1. Traitement synchrone avec try-catch, traitement asynchrone avec catchError/subscribe.error
  2. Toujours capturer les erreurs avec try-catch dans finalize
  3. Ne pas étouffer l'information d'erreur, propager ou enregistrer correctement
  4. Fournir des informations détaillées avec des classes d'erreur personnalisées
  5. Ne pas utiliser de try-catch inutiles (le code devient complexe)

Sections connexes ​

Ressources de référence ​

Publié sous licence CC-BY-4.0.