Exceptions

« Return to the Chapter Index

Table of contents
  1. Exceptions
    1. Key Idea
    2. Exceptions in Typescript
      1. Using exceptions
      2. Custom Errors
      3. Defensive Programming
      4. Exception Handling
      5. Common Pitfalls and Mistakes
    3. Summary
  2. Next Step

Key Idea

An Exception is the process of responding to the occurrence of exceptions – anomalous or exceptional conditions at run time.

Exceptions in Typescript

What is an exception?

  • An exception is a way to break the “normal” flow of a program in the event that an abnormal condition exists.
  • This can be due to invalid inputs or data provided at runtime or any other condition that is not the “common case” behavior of a method or function.
  • It is a way to respond to validation within your code in a structured way.
  • Some exception may be generated by libraries that you may use.
  • You can raise and throw exceptions within your own code
  • When an exception is thrown, the program will terminate unless the exception is caught.
let x:number=50;
throw new Error("This is an error");
console.log(x);

Note that the line console.log(x) will not execute. The current function will exit immediately and if the exception is not “handled” by a calling method somewhere in the call stack, the program will terminate immediately. We will talk about handling exceptions in a bit, but for now, we want to be able to generate them when exceptional conditions occur.

So let’s examine in detail what the above code does:

  • Sets the variable x to the value 50.
  • Immediately terminates execution of the current method and begins to “bubble up” the exception through all of the calling methods until it is handled.
  • If the exception bubbles past the first function called, the program terminates and prints an error message to the console.

If the exceptions is not handled, the program exits. The system prints out the call stack in the console.

Error: This is an error
    at test (/home/xxx/test.js:4:11)
    at Object.<anonymous> (/home/xxx/test.js:7:1)
    at Module._compile (node:internal/modules/cjs/loader:1376:14)
    at Module._extensions..js (node:internal/modules/cjs/loader:1435:10)
    at Module.load (node:internal/modules/cjs/loader:1207:32)
    at Module._load (node:internal/modules/cjs/loader:1023:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:135:12)
    at node:internal/main/run_main_module:28:49

Note: The call stack shows us all the places where we could have caught the error as well as all the internal code that is part of the Typescript system. In this example, the first 2 lines show where we could have caught the exception.

Using exceptions

We can use exceptions to improve our software design and make it react in a structured way to exceptional conditions.

Let’s consider the code for our drawing program again.

class Color {
  constructor(
    private red: number = 0,
    private green: number = 0,
    private blue: number = 0,
  ) {}
  clone(): Color {
    return new Color(this.red, this.green, this.blue);
  }
  getRed(): number {
    return this.red;
  }
  getGreen(): number {
    return this.green;
  }
  getBlue(): number {
    return this.blue;
  }
}

Valid color values in our program are numbers between 0 and 255. What happens if we try to create a color with different values?

  • The code will allow these non-sensical values to be stored in red, green and blue.

We can use exceptions to prevent this.

export class Color {
  constructor(
    private red: number = 0,
    private green: number = 0,
    private blue: number = 0,
  ) {
    if (red<0 || red>255) throw new Error(“Invalid red value”);
    if (green<0 || green>255) throw new Error(“Invalid green value”);
    if (blue<0 || blue>255) throw new Error(“Invalid blue value”);
  }
  clone(): Color {
    return new Color(this.red, this.green, this.blue);
  }
  //Rest of code removed for brevity
  . . .
  . . .
}

We can check the values in the constructor, and throw an exception if they are invalid. It will be up to the code that is creating the color object to “handle” the exception, otherwise the program will exit with an error like the one we saw previously.

Note: Now we can’t create a color objects with invalid values. If we try, the Color class will raise an exception to notify the calling code that something bad happened.

If the calling code does not “handle” the exception, then the program will terminate with an error message (the one you threw) and the call stack to help you figure out where the exception occurred in the execution of your program.

const color:Color=new Color(400, 400, 400);

Throws (raises) an exception with the message “Invalid red value”. Again, if this is not handled somewhere in the code that calls this, the program will exit.

Custom Errors

If we want to pass more information with our Error, we can create our own class that extends error, and throw that.

class ColorError extends Error {
  constructor(
    message: string,
    public red: number,
    public green: number,
    public blue: number,
  ) {
    super(message);
    this.name = "ColorError";
  }
}

Here this.name is part of the Error class which we are extending (inheritance). The message is as well which we are updating by calling super(message); then we are adding properties red, green, and blue so that they are reported to the calling method with the exception. This can be very useful when we get to exception handling.
If a block of code throws different kinds of exceptions, this can be a good way to notify the calling method as to the type of exception and can help in writing the handler.

Exceptions are useful during programming even if we don’t handle them. If you throw an exception every time the inputs to your method are wrong, or some other kind of error occurs, and you have good tests, you will see those errors and be able to fix them.

If we accidentally try to create an invalid color object, the program will terminate and tell us why. The call stack will tell us where the method was called.

There are other places in our drawing code where we are allowing an invalid or incorrect state to occur because we are not checking. Again, we can prevent this by throwing an exception when this happens.

In our polygon class, I can create polygons with no points, 1 point, or 2 points which are NOT POLYGONS. We can also create millions of polygons, perhaps we can prevent that as well. Good documentation can help, but using exceptions will prevent it. Can I use exception handling to make sure it is not possible to create an invalid polygon?

const MAX_POINTS:number = 10;
class Polygon extends Drawable {
  protected points: Point[] = [];
  constructor(points: Point[], color: Color) {
    super(color);
    if (points.length<3 || points.length>MAX_POINTS)
      throw new Error(“Invalid polygon”);
    for (let point of points) {
      this.points.push(point.clone());
    }
  }
  //Rest of class removed for brevity
  . . .
  . . .
}

Now, if I try to create a polygon with less than 3 or more than 10 points, an exception is thrown. If not, then program execution continues normally. If we don’t handle this exception, the program will terminate (letting us know to either handle the exception, or fix the calling code to prevent it.

Where else might exception handling help us find issues with our drawing program?

How about a circle with 0 or negative radius?

class Circle extends Drawable {
    private center: Point;
    private radius: number;
    constructor(center: Point, radius: number, color: Color) {
      super(color);
      if (radius<=0) throw new Error("Radius must be greater than 0");
      this.center = center.clone();
      this.radius = radius;
    }
    //Rest of class removed for brevity
    . . .
    . . .
}

A line where the two points are the same

First it might be useful to add a method to compare to points. We can then use that method to determine if two points have the same value (not the same object reference).

class Point {
  constructor(
    private x: number = 0,
    private y: number = 0,
  ) {
    super(color);
    if (start.equals(end))
      throw new Error("Start and end points must be different");
    this.start = start.clone();
    this.end = end.clone();
  }
  equals(other: Point): boolean {
    return this.x === other.x && this.y === other.y;
  }
  //Rest of class removed for brevity
  . . .
  . . .
}

Remember if a and b are Point objects, then a===b asks if they are the same object reference in memory, but a.equals(b) checks if they have the same coordinates, whether or not they are the same physical object reference.

We can use the equals to validate our line object. If the two points have the same coordinates, regardless of if they are references to the same object, the constructor will throw an exception. Now our Line is guaranteed to have start and end points with different coordinates.

Because Color throws an exception if the values are invalid, we don’t need to check that here. The call to the color constructor will throw an exception if the color is invalid, so we don’t need to worry about it here.

We could do something similar with our polygon class to verify that none of the points are the same. This would also handle things for our Rectangle and Triangle classes since they are now derived from Polygon

class Polygon extends Drawable {
	protected points: Point[] = [];
	constructor(points: Point[], color: Color) {
		super(color);
		if (points.length < 3 || points.length > MAX_POINTS)
			throw new Error(
				`A polygon must have at least 3 points and at most ${MAX_POINTS} points`,
			); 	  
		// Check for duplicate points
		for (let i = 0; i < points.length; i++) {
			for (let j = i + 1; j < points.length; j++) {
				if (points[i].equals(points[j])) {
					throw new Error("Duplicate points are not allowed in a polygon.");
				}
			}
		}
		
		for (let point of points) {
			this.points.push(point.clone());
		}
	}
  //Rest of class removed for brevity
  . . .
  . . .
}    

Note the Brute force approach to searching for duplicates. For each element, check all the remaining elements for duplicates. Also note that we still need to make sure there are at least 3 and not more than MAX_POINTS points in the polygon. Now we are also making them unique.

Thought Question: Why does j start at i+1 and not 0?

Defensive Programming

So now we can prevent our code from being exposed to “exceptional” or invalid operation, by simply throwing an exception when those cases arise. If we write good test cases, we will find errors in our code, but right now, our program will just exit with an error message.
Making sure that our code will not accept invalid values and thus have undocumented, or undefined behaviors is good defensive programming. It would be better if we were able to catch the exception somewhere in the call stack and handle it elgently instead of just having our program crash with an error message just because of some invalid input. At a minimum it would be nice to exit cleanly and report the problem to the user in a more “user friendly” way.

Exception Handling

We can use the try/catch/finally approach to handle errors thrown by methods that we call.

try {
  //do something which might throw an exception
} catch (e) {
  //handle the exception in some way
}finally{
  //do something after regardless of the try/catch result
}

If we do one or more operations which might throw an error within a try block, if an exception occurs within that code or any code that is called within the block, that code exits immediately, and the catch block is called, where e is the Error derived object that was passed to throw within the code. This will prevent the program from exiting and consume the exception and the program will continue normally after the try/catch/finally block. You can rethrow the error in the catch block, which will continue to “bubble up” the exception so our caller can handle the error after we recognize it (maybe we log, then rethrow).

let color: Color;
let line: Line;
const start = new Point(100, 100);
const end = new Point(200, 200);
try {
	color = new Color(0, green, 0);
} catch (e) {
	console.log(e);
    color = new Color();
} finally {
	line = new Line(start, end, color);
}

Here we try to create a color. If the color is valid, it is created, if not, the error is logged to the console, and a default color object is created. The finally block runs after either way. It creates a line with the newly defined color. We have handled the exception and our code will work, even if the value of green is invalid. It will either create a green line if green>=0 && green<=256 or the default colored line if not.

let color: Color;
let line: Line;
const start = new Point(100, 100);
const end = new Point(200, 200);
try {
	color = new Color(0, green, 0);
} catch (e) {
	console.log(e);
  color = new Color();
}
line = new Line(start, end, color);

A note about finally. In this code it is not necessary since the code continues after the try/catch either way, so we can remove it and just let the program continue with creating the line. There are many use cases where we don’t need a finally block, but there are some where we do.

Here is a case where finally is useful:

import * as fs from "fs";
let fileDescriptor: number;
let fileContents: string;
try {
	fileDescriptor = fs.openSync("test.txt", "r");
} catch (e) {
	throw new Error("Could not open file test.txt");
}
try {
	fileContents = fs.readFileSync(fileDescriptor, "utf8");
} catch (e) {
	throw new Error("Could not read file test.txt");
} finally {
	console.log("Closing file");
	fs.closeSync(fileDescriptor);
}
console.log(fileContents);

If I have an open file, and encounter an error while reading it, we want to rethrow the exception, but first we want to close the file. This code opens the file, tries to read it, and regardless of success or not, closes the file. On success it prints the contents, and on error it throws an exception

Common Pitfalls and Mistakes

  • Throwing a string instead of an Error: Allowed but bad form
  • Using exceptions to communicate non-exceptional situations. These are designed for expressing error conditions, and should not be used as a way to return data in normal execution.
  • If we want the exception to continue to bubble, we must rethrow it, or throw a new exception of our own. throw e or throw new Error(“This is my error”)

Summary

In summary, when writing our code we should program defensively. When a method or code block accepts input, throw an exception if the input is not valid. We can override (extend) the Error class to create our own more detailed Error classes for our exceptions. The thrown exception will “bubble up” through the code that called the code that threw the exception, all the way to the top of the call stack. If nothing handles it, then the program terminates and displays the exception and the full call stack. We can catch a thrown exception with the try/catch or try/catch/finally constructs. These consume the exception (stop bubbling).

Next Step

Next we’ll learn about comments: Comments »


Back to top

Created by Greg Silber, Austin Cory Bart, James Clause.