1

Background:
On a project I am working on, we've switched from using AngularJS (1.6.2) with JavaScript, to TypeScript 2.1.5.

We have a decorator applied to the $exceptionHandler service that causes JavaScript exceptions to make a call to a common API that send the development team an e-mail; in this way, we can easily see what front-end errors our end-users are encountering in the wild.

Problem:
I recently converted this decorator from JavaScript, to TypeScript. When I try to run the application, I encounter a whitescreen of nothing. After much debugging I discovered that the issue is because AngularJS expects the $provide.decorator to pass a function along with a list of dependencies. However, an object is instead being observed, and thus forcing Angular to fail-safe.

I diagnosed the problem by setting breakpoints inside of angular.js itself; it specifically will fail on line 4809 (inside of function createInternalInjector(cache, factory)) due to a thrown, unhandled JavaScript exception, but the part that's actually responsible for the failure is line 4854, inside of function invoke(fn, self, locals, serviceName). The reason it fails, is because the dependencies passed come across as ['$delegate', '$injector',]; the function is missing from this set.

Lastly, one thing I considered doing was simply defining a JavaScript function in the class code. This does not work in my case for two reasons. First, in our ts.config, we have noImplicitAny set to true; functions are implicitly of the any type. Additionally, TypeScript itself appears not to recognize function as a keyword, and instead tries and fails to compile it as a symbol on class ExceptionHandler.

TypeScript Exception Handler:

export class ExceptionHandler {
    public constructor(
        $provide: ng.auto.IProviderService
    ) {
        $provide.decorator('$exceptionHandler`, [
            '$delegate',
            '$injector',
            this.dispatchErrorEmail
        ]);
    }

    public dispatchErrorEmail(
        $delegate: ng.IExceptionHandlerService,
        $injector: ng.auto.IInjectorService
    ): (exception: any, cause: any) => void {
        return (exception: any, cause: any) => {
            // First, execute the default implementation.
            $delegate(exception, cause);

            // Get our Web Data Service, an $http wrapper, injected...
            let webDataSvc: WebDataSvc = $injector.get<WebDataSvc>('webDataSvc');

            // Tell the server to dispatch an email to the dev team.
            let args: Object = {
                'exception': exception
            };
            webDataSvc.get('/api/common/errorNotification', args);
        };
    }
}

angular.module('app').config(['$provide', ExceptionHandler]);

Original JS:

(function () {
    'use strict';

    angular.module('app').config(['$provide', decorateExceptionHandler]);

    function decorateExceptionHandler($provide) {
        $provide.decorator('$exceptionHandler', ['$delegate', '$injector', dispatchErrorEmail]);
    }

    function dispatchErrorEmail($delegate, $injector) {
        return function (exception, cause) {
            // Execute default implementation.
            $delegate(exception, cause);

            var webDataSvc = $injector.get('webDataSvc');

            var args = {
                'exception': exception,
            };
            webDataSvc.get('/api/common/ErrorNotification', args);
        };
    }
})();

Questions:
1. In what way can I rewrite the TypeScript Exception Handler to be properly picked up by AngularJS?
2. If I can't, is this an AngularJS bug that needs to be escalated? I know for a fact I'm not the only person using AngularJS with TypeScript; being unable to decorate a service due to language choice seems like a pretty major problem.

2
  • Why are you doing this? Converting everything to classes isn't the purpose of TS. config expects a callback function, not a constructor. And $exceptionHandler is factory function, not a class, too. There's just no place for classes. Commented Mar 13, 2017 at 13:59
  • I did it this way, because based on my (limited) knowledge of TypeScript, it seems like the correct transcription. If it's not, then I need to know - I invite you to provide an answer, per my questions. Commented Mar 13, 2017 at 14:02

2 Answers 2

2

Converting everything to classes isn't the purpose of TS, there's no use for classes here. JS code is supposed to be augmented with types and probably be enhanced with $inject annotation.

angular.module('app').config(decorateExceptionHandler);

decorateExceptionHandler.$inject = ['$provide'];

export function decorateExceptionHandler($provide: ng.auto.IProviderService) {
    $provide.decorator('$exceptionHandler', dispatchErrorEmail);
}

dispatchErrorEmail.$inject = ['$delegate', '$injector'];

export function dispatchErrorEmail(
    $delegate: ng.IExceptionHandlerService,
    $injector: ng.auto.IInjectorService
): (exception: any, cause: any) => void { ... }

config expects a regular function, not a constructor. And the reason why the original TS code fails is that ExceptionHandler isn't called with new, thus this is not an object, and this.dispatchErrorEmail is not a function.

Sign up to request clarification or add additional context in comments.

4 Comments

That did it. I didn't know that functions could be exported in a manner similar to classes. Also, my background is as a C# programmer - I'm used to thinking nigh-purely in terms of putting stuff in classes. Thanks for expanding my horizons.
@AndrewGray You're welcome. ES6/TS classes are just syntax sugar for constructor functions. A1 wasn't written with classes in mind, so it heavily uses factory functions. But consider using classes for service services and controllers. Btw, there's no real need to export them, they could be just anonymous arrow functions. But exporting them benefits testing a bit.
Going one step further from this answer, I did a little research on my own code. Part of my bad assumptions, was that the class keyword maps to the IIFE pattern used in the JavaScript I transcribed from; this is not the case. Rather, the IIFE instead corresponds (very roughly) to either a namespace, or a module instead.
Absolutely. ES6 modules deprecate IIFEs, because module scope is local.
1

Here's another way to do it using TypeScript namespaces. It feels a little cleaner to me, keeping the exception extension functions isolated, and is a nice conceptually coming from something like C#.

exception.module.ts

import * as angular from 'angular';
import { LoggerModule } from '../logging/logger.module';
import { ExceptionExtension } from './exception.handler';

export const ExceptionModule = angular.module('app.common.exception', [LoggerModule])
    .config(ExceptionExtension.Configure)
    .name;

exception.handler.ts

import { ILoggerService } from '../logging/logger.service';

export namespace ExceptionExtension {
    export const ExtendExceptionHandler = ($delegate: ng.IExceptionHandlerService, logger: ILoggerService) => {
        return function (exception: Error, cause?: string): void {
            $delegate(exception, cause);
            logger.error(exception.message, "Uncaught Exception", cause ? cause : "");
        }
    };
    ExtendExceptionHandler.$inject = ['$delegate', 'ILoggerService'];

    export const Configure = ($provide: ng.auto.IProvideService) => {
        $provide.decorator('$exceptionHandler', ExtendExceptionHandler);
    };
    Configure.$inject = ['$provide'];
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.