CHAPTER 7

image

Exceptions, Memory, and Performance

The primary duty of an exception handler is to get the error out of the lap of the programmer and into the surprised face of the user. Provided you keep this cardinal rule in mind, you can’t go far wrong.

—Verity Stob

Despite lacking the appeal of language features or runtime environments, understanding exceptions and memory management will help you to write better TypeScript programs. Exceptions in JavaScript and TypeScript may look familiar to programmers who have used C#, Java, PHP, or many other languages, but there are some subtle yet important differences. The topics of exception handling and memory management are inextricably linked because they share a language feature, which is described later in this chapter.

The subject of memory management is often dominated by folklore, falsehoods, and blindly applied best practices. This chapter deals with the facts of memory management and garbage collection and explains how you can take measurements to test optimizations, rather than applying a practice that may make little or no difference (or even perform worse than the original code). This will lead briefly to the subject of performance.

Exceptions

Exceptions are used to indicate that a program or module is unable to continue processing. By their very nature, they should only be raised in truly exceptional circumstances. Often, exceptions are used to indicate that the state of the program is invalid and is not safe to continue.

Although it can be tempting to start issuing exceptions every time a routine is passed a disagreeable value as an argument, it can often be more graceful to handle input that you can anticipate without raising an exception.

When your program encounters an exception, it will be shown in the JavaScript console unless it is handled in code. The console allows programmers to write messages, and it will automatically log any exceptions that occur while running the program.

You can inspect the console for exceptions in all modern web browsers. The shortcut key differs from browser to browser and varies by platform, but if CTRL + SHIFT + I fails to work on your Windows or Linux machine or CMD + OPT + I fails on your Mac, you can usually find the tools in the browser’s menu listed under “Developer Tools, Browser Console” or a similar name. For Node, the error and warning output will appear in the command window you use to run the HTTP server.

Throwing Exceptions

To raise an exception in your TypeScript program you use the throw keyword. Although you can follow this keyword with any object, it is best to provide either a string containing an error message, or an instance of the Error object wrapping the error message.

Listing 7-1 shows a typical exception being thrown to prevent an unacceptable input value. When the errorsOnThree function is called with a number, it returns the number, unless it is called with the number three, in which case the exception is raised.

Listing 7-1. Using the throw keyword

function errorsOnThree(input: number) {
    if (input === 3) {
        throw new Error('Three is not allowed'),
    }

    return input;
}

var result = errorsOnThree(3);

The general Error type in this example can be replaced with a custom exception. You can create a custom exception using a class that implements the Error interface as shown in Listing 7-2. The Error interface ensures that your class has a name and message property.

The toString method in Listing 7-2 is not required by the Error interface, but is used in many cases to obtain a string representation of the error. Without this method, the default implementation of toString from Object would be called, which would write “[object Object]” to the console. By adding the toString method to the ApplicationError class you can ensure that an appropriate message is shown when the exception is thrown and logged.

Listing 7-2. Custom error

class ApplicationError implements Error {

    public name = 'ApplicationError';

    constructor(public message: string) {
        if (typeof console !== 'undefined') {
            console.log('Creating ' + this.name + ' "' + message + '"'),
        }
    }

    toString() {
        return this.name + ': ' + this.message;
    }
}

You can use custom exceptions in a throw statement to classify the kind of error that has occurred. It is a common exception pattern to create a general ApplicationError class and inherit from it to create more specific kinds of errors. Any code that handles exceptions is then able to take different actions based on the type of error that has been thrown, as demonstrated in the later section on exception handling.

Listing 7-3 shows a specific InputError class that inherits from the ApplicationError class. The errorsOnThree function uses the InputError exception type to highlight that the error has been raised in response to bad input data.

Listing 7-3. Using inheritance to create special exception types

class ApplicationError implements Error {

    public name = 'ApplicationError';

    constructor(public message: string) {
    }

    toString() {
        return this.name + ': ' + this.message;
    }
}

class InputError extends ApplicationError {
}

function errorsOnThree(input: number) {
    if (input === 3) {
        throw new InputError('Three is not allowed'),
    }

    return input;
}

The InputError in the example simply extends the ApplicationError; it doesn’t need to implement any of the properties or methods as it only exists to provide a category of exceptions to use within your program. You can create exception classes to extend ApplicationError, or to further specialize a subclass of ApplicationError.

Image Note  You should treat the native Error type as sacred and never throw an exception of this kind. By creating custom exceptions as subclasses of the ApplicationError class, you can ensure that the Error type is reserved for use outside of your code in truly exceptional cases.

Exception Handling

When an exception is thrown, the program will be terminated unless the exception is handled. To handle an exception you can use a try-catch block, a try-finally block, or even a try-catch-finally block. In any of these cases, the code that may result in an exception being thrown is wrapped within the try block.

Listing 7-4 shows a try-catch block that handles the error from the errorsOnThree function in the previous section. The parameter accepted by the catch block represents the thrown object, for example, the Error instance or the custom ApplicationError object, depending on which one you used in the throw statement.

Listing 7-4. Unconditional catch block

try  {
    var result = errorsOnThree(3);
} catch (err) {
    console.log('Error caught, no action taken'),
}

The err parameter is scoped to the catch block, making it one of the few variables you will encounter that doesn’t follow the normal function scoping rules. This parameter behaves as if it was a variable declared with the let keyword, rather than the var keyword, as described in Chapter 4.

It is common in languages that support try-catch blocks to allow specific exception types to be caught. This allows the catch block to only apply to specific types of exceptions and for other types of exceptions to behave as if there were no try-catch block. This technique is recommended to ensure that you only handle exceptions that you know you can recover from, leaving truly unexpected exceptions to terminate the program and prevent further corruption of the state.

There is currently no standards-compliant method of conditionally catching exceptions, which means you catch all or none. If you only wish to handle a specific type of exception, you can check the type within the catch statement and re-throw any errors that do not match the type.

Listing 7-5 shows an exception handling routine that handles ApplicationError custom exceptions, but re-throws any other type.

Listing 7-5. Checking the type of error

try  {
    var result = errorsOnThree(3);
} catch (err) {
    if (!(err instanceof ApplicationError)) {
        throw err;
    }

    console.log('Error caught, no action taken'),
}

Image Note  By handling only custom exceptions, you can ensure that you are only handling the types of exceptions that you know you can recover from. If you use the default catch block with no instanceof check, you are taking responsibility for every type of exception that may occur within your program.

This example will allow the catch block to handle an ApplicationError, or a subclass of ApplicationError such as the InputError described earlier in this chapter. To illustrate the effect of handling exceptions at different levels in the class hierarchy, Figure 7-1 shows a more complex hierarchy that extends the ApplicationError and InputError classes.

9781430267911_Fig07-01.jpg

Figure 7-1. Error class hierarchy

When you choose to handle the InputError category of exceptions, you will be handling four kinds of exceptions as shown in Figure 7-2: InputError, BelowMinError, AboveMaxError, and InvalidLengthError. All other exceptions will be passed up the call stack as if they were unhandled.

9781430267911_Fig07-02.jpg

Figure 7-2. Handling InputError exceptions

If you were to handle the ApplicationError category of exceptions, you would be handling all seven custom exceptions in the hierarchy as shown in Figure 7-3.

9781430267911_Fig07-03.jpg

Figure 7-3. Handling ApplicationError exceptions

Generally speaking, the exceptions that you handle should be more specific the deeper you are into your program. If you were working near low level code, you would handle very specific types of exceptions. When you are working closer to the user interface, you would handle more general kinds of exceptions.

Exceptions will crop up again shortly with the discussion on performance because there is a performance cost associated with creating and handling exceptions in your program. Despite this, if you are using them only to signal when the routine cannot continue, you shouldn’t worry about their runtime cost.

Memory

When you write your program in a high-level language such as TypeScript or JavaScript, you will benefit from automatic memory management. All of the variables and objects you create will be managed for you, so you will never overrun a boundary or have to deal with a dangling pointer or corrupted variable. In fact, all of the manageable memory problems you could encounter are already handled for you. There are, however, some classes of memory safety that cannot be handled automatically, such as out of memory errors that indicate the system’s resources have been exhausted and it is not possible to continue processing.

This section covers the types of problems that you may encounter and what you need to know to avoid them.

Releasing Resources

In TypeScript, you are unlikely to encounter unmanaged resources. Most APIs follow the asynchronous pattern, accepting a method argument that will be called when the operation completes. Therefore, you will never hold a direct reference to the unmanaged resource in your program. For example, if you wanted to use the proximity API, which detects when an object is near the sensor, you would use the code in Listing 7-6.

Listing 7-6. Asynchronous pattern

var sensorChange = function (reading) {
    var proximity = reading.near ?
        'Near' : 'Far';
    alert(proximity);
}

window.addEventListener('userproximity', sensorChange, true);

The asynchronous pattern means that although you can obtain information from the proximity sensor, your program is never responsible for the resource or communication channel. If you happen to encounter a situation where you do hold a reference to a resource that you must manage, you should use a try-finally block to ensure that the resource is released, even if an error occurs.

The example in Listing 7-7 assumes that it is possible to work directly with the proximity sensor to obtain a reading.

Listing 7-7. Imaginary unmanaged proximity sensor

var sensorChange = function (reading) {
    var proximity = reading.near ?
        'Near' : 'Far';
    alert(proximity);
}

var readProximity = function () {
    var sensor = new ProximitySensor();
    try {
        sensor.open();

        var reading = sensor.read();

        sensorChange(reading);
    } finally {
        sensor.close();
    }
}

window.setInterval(readProximity, 500);

The finally block will ensure the sensor’s close method is called, which performs the cleanup and releases any resources. The finally block executes even if there is an error calling the read method or the sensorChange function.

Garbage Collection

When memory is no longer needed, it needs to be freed for it to be allocated to other objects in your program. The process used to determine whether memory can be freed is called garbage collection. There are several styles of garbage collection that you will encounter depending on the runtime environment.

Older web browsers may use a reference-counting garbage collector, freeing memory when the number of references to an object reaches zero. This is illustrated in Table 7-1. This is a very fast way to collect garbage as the memory can be freed as soon as the reference count reaches zero. However, if a circular reference is created between two or more objects, none of these objects will ever be garbage-collected because their count will never reach zero.

Table 7-1. Reference counting garbage collection

Object

Reference Count

Memory De-Allocated

Object A

1

No

Object B

1

No

Object C

1

No

Object D

1

No

Object E

0

Yes

Modern web browsers solve this problem with a mark-and-sweep algorithm that detects all objects reachable from the root and garbage collects the objects that cannot be reached. Although this style of garbage collection can take longer, it is less likely to result in memory leaks.

The same objects from Table 7-1 are shown in Figure 7-4. Using the reference-counting algorithm both Object A and Object B remain in memory because they reference each other. These circular references are the source of memory leaks in older browsers, but this problem is solved by the mark-and-sweep algorithm.  The circular reference between Object A and Object B is not sufficient for the objects to survive garbage collection as only objects that are accessible from the root remain.

9781430267911_Fig07-04.jpg

Figure 7-4. Mark and sweep

The use of mark-and-sweep garbage collection algorithms means that you rarely need to worry about garbage collection or memory leaks in your TypeScript program.

Performance

There is no doubt that the grail of efficiency leads to abuse. Programmers waste enormous amounts of time thinking about, or worrying about, the speed of noncritical parts of their programs, and these attempts at efficiency actually have a strong negative impact when debugging and maintenance are considered. We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil. Yet we should not pass up our opportunities in that critical 3%.

—Donald Knuth

This is not the first time Donald Knuth (“Structured Programming With go to Statements,” Computing Surveys, 1974) has been quoted with respect to performance and optimization and it certainly won’t be the last. His words, at least in this respect, have stood the test of time (even though they came from a paper defending GOTO statements—a sentiment that has somewhat fallen flat over the course of time).

If the question of performance arises before there is a measurable performance problem, you should avoid optimization. There are many articles that claim that using local variables will be faster than global variables, that you should avoid closures because they are slow, or that object properties are slower than variables. Although these may be generally true, treating them as design rules will lead to a poor program design.

The golden rule of optimization is that you should measure the difference between two or more potential designs and decide if the performance gains are worthy of any design trade-offs you must make to gain them.

Image Note  When it comes to your TypeScript program, measuring execution time requires running tests on multiple platforms. Otherwise you may be getting faster in one browser, but slower in another.

The code in Listing 7-8 will be used to demonstrate a simple performance test. The lightweight CommunicationLines class will be tested. The class contains a single method that takes in a teamSize and calculates the number of lines of communication between team members using the famous n(n–1)/2 algorithm. The function named testCommunicationLines instantiates the class and successfully tests two cases for team sizes of 4 and 10, which have 6 and 45 lines of communication respectively.

Listing 7-8. Calculating lines of communication

class CommunicationLines {
    calculate(teamSize: number) {
        return (teamSize * (teamSize - 1)) / 2
    }
}

function testCommunicationLines() {
    var communicationLines = new CommunicationLines();

    var result = communicationLines.calculate(4);

    if (result !== 6) {
        throw new Error('Test failed for team size of 4.'),
    }

    result = communicationLines.calculate(10);

    if (result !== 45) {
        throw new Error('Test failed for team size of 10.'),
    }
}

testCommunicationLines();

The Performance class in Listing 7-9 wraps a callback function in a method that uses the performance.now method to time the operation using the high-fidelity timer discussed in Chapter 4. To get a fair measurement, the Performance class runs the code 10,000 times by default, although this number can be overridden when the run method is called.

The output from the Performance class includes the total time taken to execute the code 10,000 times as well as the average time per iteration.

Listing 7-9. Performance.ts runner

class Performance {
    constructor(private func: Function, private iterations: number) {

    }

    private runTest() {
        if (!performance) {
            throw new Error('The performance.now() standard is not supported in this runtime.'),
        }

        var errors: number[] = [];

        var testStart = performance.now();

        for (var i = 0; i < this.iterations; i++) {
            try {
                this.func();
            } catch (err) {
                // Limit the number of errors logged
                if (errors.length < 10) {
                    errors.push(i);
                }
            }
        }

        var testTime = performance.now() - testStart;

        return {
            errors: errors,
            totalRunTime: testTime,
            iterationAverageTime: (testTime / this.iterations)
        };
    }

    static run(func: Function, iterations = 10000) {
        var tester = new Performance(func, iterations);
        return tester.runTest();
    }
}

export = Performance;

To use the Performance class to measure the program, the code must be imported and the call to the testCommunicationLines function replaced by passing the function into the run method of the Performance class, as shown in Listing 7-10.

Listing 7-10. Running the performance test

import perf = require('./performance'),

class CommunicationLines {
    calculate(teamSize: number) {
        return (teamSize * (teamSize - 1)) / 2
    }
}

function testCommunicationLines() {
    var communicationLines = new CommunicationLines();

    var result = communicationLines.calculate(4);

    if (result !== 6) {
        throw new Error('Test failed for team size of 4.'),
    }

    result = communicationLines.calculate(10);

    if (result !== 45) {
        throw new Error('Test failed for team size of 10.'),
    }
}

var result = perf.run(testCommunicationLines);

console.log(result.totalRunTime + ' ms'),

The result of this code is that a total run time of 2.73 ms is logged to the console. This means that the entire run of 10,000 iterations (which is 20,000 calls to the communication lines algorithm) takes less than 3 ms. In most cases, a result such as this is a good indication that you are looking in the wrong place for optimization opportunities.

It is possible to get a very different result by adjusting the code as shown in Listing 7-11. The only change made to the code is to check the call to communicationLines.calculate with a team size of four results in seven communication lines. This test will fail and an exception will be raised.

Listing 7-11. Running the performance test with exceptions

import perf = require('./performance'),

class CommunicationLines {
    calculate(teamSize: number) {
        return (teamSize * (teamSize - 1)) / 2
    }
}

function testCommunicationLines() {
    var communicationLines = new CommunicationLines();

    var result = communicationLines.calculate(4);

    // This test will now fail
    if (result !== 7) {
        throw new Error('Test failed for team size of 4.'),
    }

    result = communicationLines.calculate(10);

    if (result !== 45) {
        throw new Error('Test failed for team size of 10.'),
    }
}

var result = perf.run(testCommunicationLines);

console.log(result.totalRunTime + ' ms'),

Running the code with the failing test and the creation and handling of an exception results in a total run of 214.45 ms—this is 78 times slower than the first test. It is possible to use this data to guide your design decisions. You may want to repeat the tests multiple times or try different iteration sizes to ensure you get consistent results.

Here are some numbers collected using the Performance class from Listing 7-9 to evidence the claims made in respect of optimization at the start of this section. Using a simple, but limited test with a baseline time of 0.74 ms per iteration, the results were as follows (where higher numbers indicate slower execution times):

  • Global variables: 0.80 ms (0.06 ms slower per iteration)
  • Closures: 1.13 ms (0.39 ms slower per iteration)
  • Properties: 1.48 ms (0.74 ms slower per iteration)

Over 10,000 executions you can see small differences in the execution times, but it is important to remember that your program will return different results due to differences such as object complexity, nesting depth, number of object created, and many other factors. Before you make any optimizations, make sure you have taken measurements.

Summary

This chapter has covered three important topics that are likely to be fundamental to any large application written in TypeScript. In most cases, these three areas are likely to be cross-cutting concerns that may be easier to consider before you write a large amount of code that needs to be changed.

Using exceptions to handle truly exceptional states in your program prevents further corruption of the program data. You should create custom exceptions to help manage different kinds of errors and test the types in your catch blocks to only handle errors that you know you can recover from.

Modern runtimes all handle memory using the reliable mark-and-sweep algorithm, which does not fall victim to the circular reference memory leak that older reference-counting garbage collectors suffer from. It is generally accepted that programmers don’t need to code with garbage collection in mind, but if you can measure a performance problem and discover that garbage collection is the issue, you may decide to help the garbage collector by creating less objects for it to manage.

Whenever you are working on optimization, you should first measure the performance of your program to prove whether your assumption about optimization is correct when making a change. You should measure your changes in multiple environments to ensure you improve the speed in all of them.

Key Points

  • You can use the throw keyword with any object, but it is best to use subclasses of a custom error.
  • You can handle exceptions with try-catch-finally blocks, where you must specify either a catch or finally block, or both if you wish.
  • You can’t reliably catch only custom exceptions, but you can test the exception type within the catch block.
  • Most APIs you encounter will follow the asynchronous pattern, but if you find you have to manage a resource, use a try-finally block to clean up.
  • When it comes to performance, you need before and after measurements to back up any code you change in the name of optimization.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.227.134.154