TypeScript Error Handling with Union Types

One of the challenges of modern software development is handling errors in elegant ways. Fortunately for TypeScript users, there is a clean way to indicate whether or not a function encountered an error by using Union Types.

Let’s consider an simple example where there is a table in a database that contains information about programmers. We have a function that intends to take the programmer’s name as an argument, lookup the programmer in the database, and return the programmer’s favorite programming language. In an ideal world, the code could look like this. (The actual database lookup has been omitted for brevity.)

import {Programmer} from "./Programmer"

export class ProgrammerRepository{
    public GetByName(name : String) : Programmer{
        // stub for more interesting data retrieval
        return new Programmer("Robert Tables", "TypeScript");
    }
}

// Client Code //

export function GetFavoriteLanguage(name : String) : String{
    var repo = new ProgrammerRepository();
    return repo.GetByName(name).GetFavoriteLanguage();
}

This code is simple and straightforward, but it doesn’t do anything to handle error cases. As any experienced programmer knows, looking up data in a database can fail in a large number of ways.

Throwing Errors

The traditional way to handle failures in JavaScript is to throw Errors. In our simplified example, this would mean that if the repository code couldn’t connect to the database, couldn’t find the record in the database, or some related error, the repository would throw an Error.

import {Programmer} from "./Programmer"

export class ProgrammerRepository{
    public GetByName(name : String) : Programmer{
        // stub for more interesting data retrieval
        throw new Error("data storage error");
    }
}

// Client Code //

export function GetFavoriteLanguage(name : String) : String{
    var repo = new ProgrammerRepository();
    var favLang : String;

    try{
        favLang = repo.GetByName(name).GetFavoriteLanguage();
    } catch {
        favLang = "Could not be found"
    }
    return favLang;
}

This implementation of GetByName will throw an Error if the lookup fails for any reason. This technique functions properly and many developers are used to this style of error handling.

One potential problem with this style of error handling is that the client code is not forced to handle the error. The client code could ignore the potential exception, which could lead to unhandled exceptions in production.

A second potential problem is that the signature of the GetByName function does not indicate what kinds of errors might be thrown.

Returning a Union Type

The TypeScript type system supports a mechanism called Union Types. Union Types specify that an object will be instance of one of the types that are joined together in the union.

On line 4 of the following example, a union type named ProgrammerLookupResult is defined as the union of Programmer and LookupFailed. All of the instances of ProgrammerLookupResult will either be an instance of a Programmer or an instance of the LookupFailed class.

import {Programmer} from "./Programmer"
import {LookupFailed} from "./LookupFailed"

export type ProgrammerLookupResult = Programmer | LookupFailed;

export class ProgrammerRepository{
    public GetByName(name : String) : ProgrammerLookupResult {
        // stub for more interesting data retrieval
        return new LookupFailed("Entity not found");
    }
}

// Client Code //

export function GetFavoriteLanguage(name : String) : String{
    var repo = new ProgrammerRepository();
    var favLang : String = "";

    let result = repo.GetByName(name);
    
    if(result instanceof Programmer){
        favLang = result.GetFavoriteLanguage();
    } else if (result instanceof LookupFailed) {
        favLang = "Could not be found";
    }
    return favLang;
}

This implementation of GetByName returns an instance of ProgrammerLookupResult. If the lookup succeeded, it will be an instance of Programmer. Otherwise it will be an instance of LookupFailed.

The first thing to note is that TypeScript will not allow the client code to access the Programmer specific methods, like GetFavoriteLanguage until the type has been checked with a Type Guard. On line 21 of this example, the function checks the type of result. If this evaluates to true, the code on line 22 can call GetFavoriteLanguage without getting a compiler error.

This solves the two potential problems posed by the implementation with throwing Errors. The client code must check the type of the result, otherwise he code cannot compile. This results in fewer unhandled errors. Secondly, because it is easy to look at the definition of the union, it is clear to the developer writing the client code exactly which kinds of errors can be returned.

It is worth noting that the union type could be defined in line with the function. This would work identically to the previous example and it would make the code slightly shorter. I prefer to have the union type defined explicitly since in makes the intent of the union type clearer.

export class ProgrammerRepository{
    public GetByName(name : String) : Programmer | LookupFailed {
        // stub for more interesting data retrieval
        return new LookupFailed("Entity not found");
    }
}

Conclusion

Using Union Types to return errors from functions makes it clear to their clients what errors might be encountered. It guides them in how the potential errors must be handled. The result is more reliable software with fewer unhandled errors.

To view all code used in this post, please visit the GitHub repository .

我来评几句
登录后评论

已发表评论数()

相关站点

热门文章