Search code examples
angulartypescriptrxjszonejs

Angular Zoneless detect external code changes


I am using a global error handler and a Graphql provider that handles all requests and errors. However, when throwing an error, it is not caught by the global handler as it is outside it's angular. Throwing an error inside a component works though.

I'm not using ZoneJs.

Here is my Graphql provider code:

import { Apollo, APOLLO_OPTIONS } from 'apollo-angular';
import { HttpLink } from 'apollo-angular/http';
import { ApplicationConfig, inject } from '@angular/core';
import { ApolloClientOptions, InMemoryCache, split } from '@apollo/client/core';
import { environment } from '../environments/environment';
import { onError } from '@apollo/client/link/error';
import { Router } from '@angular/router';
import { HttpHeaders } from '@angular/common/http';
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { getMainDefinition } from '@apollo/client/utilities';
import { createClient } from 'graphql-ws';

// @ts-ignore
import  extractFiles from 'extract-files/extractFiles.mjs';
// @ts-ignore
import isExtractableFile from 'extract-files/isExtractableFile.mjs';

const uri = `${environment.base}/graphql`

export function apolloOptionsFactory(): ApolloClientOptions<any> {
  const httpLink = inject(HttpLink);
  const router = inject(Router)

  const wsLink = new GraphQLWsLink(
    createClient({
      url: (environment.base).toString()
    })
  )

  const errorLink = onError(({ graphQLErrors, networkError }) => {

    if (graphQLErrors) {
      graphQLErrors.forEach(e => {
        if (e.message === 'Unauthorized')
        router.navigate(['auth/login'])
        throw new Error(e.message)
      })
    }

    if (networkError) {
      throw new Error(networkError.message)
    }
  })
  
  return {
    link: split(
      ({ query }) => {
        const definition = getMainDefinition(query);
        return (
          definition.kind === 'OperationDefinition' &&
          definition.operation === 'subscription'
        )
      },
      wsLink,
      errorLink.concat(httpLink.create({ 
        uri,
        extractFiles: (body) => extractFiles(body, isExtractableFile),
        headers: new HttpHeaders({ 'x-apollo-operation-name': 'some-header-with-file-upload'})
      }))
    ),
    cache: new InMemoryCache()
  }
}

export const graphqlProvider: ApplicationConfig['providers'] = [
  Apollo,
  {
    provide: APOLLO_OPTIONS,
    useFactory: apolloOptionsFactory,
  },
];

Error handling service

import { ErrorHandler, Injectable, Injector, inject } from "@angular/core";
import { HttpErrorResponse } from "@angular/common/http";
import { ErrorService } from "./error.service";
import { NotificationService } from "./notification.service";

@Injectable()
export class GlobalErrorHandler implements ErrorHandler {

  injector: Injector = inject(Injector)

  handleError(error: Error | HttpErrorResponse): void {
    console.log('SOMETHING SHOULD HAPPEN SINCE ALL ERRORS ARE HANDLED HERE')
    const errorService = this.injector.get(ErrorService)
    const notifier = this.injector.get(NotificationService)

    let message: string
 
    message = errorService.getClientErrorMessage(error)
    notifier.showError(message)
  }
}

Graphql methods service

export class GraphqlService {

  public postsQuery!: QueryRef<any>
  private readonly apollo = inject(Apollo)

  public mutateFn(mutation: TypedDocumentNode<unknown, any>, variables?: any, isFile: boolean = false): Observable<MutationResult<unknown> | any> {
    return this.apollo.mutate({ 
      mutation: mutation,
      variables: variables, 
      errorPolicy: 'all',
      context: {
        useMultipart: isFile
      }
    }).pipe(map((v) => {
        return v.data
      })
    )
  }

I've tried using ApplicationRef.tick() but I'm getting an error ApplicationRef.tick() is running recursively

Is there a better way to do this?

EDIT**

Updated code following Naren's suggestion

// @ts-ignore
  const errorCallback =  ({ graphQLErrors, networkError }) => {

    if (graphQLErrors) {
      graphQLErrors.forEach((e: any) => {
        if (e.message === 'Unauthorized')
        router.navigate(['auth/login'])
        throw new Error(e.message)
      })
    }

    if (networkError) {
      throw new Error(networkError.message)
    }
  }
  
  // @ts-ignore
  const errorLink = onError(errorCallback.bind(this))

Solution

  • Angular's error handler isn't able to catch any error that happens outside of angular when running a zoneless app.

    What you could do though, is capturing global errors is your own handler with

    @Injectable()
    export class GlobalErrorHandler implements ErrorHandler {
        
       constructor() {
          window.addEventListener('unhandledrejection', event => {
             this.handleError(event.error);
             event.preventDefault()
          });
       }
    
       handleError(error : any) : void {
          ...
       }
    }