Jeff Caldwell

/blog/go-is-changing-my-js

Go is changing how I write JavaScript

Learning Go is affecting how I write JavaScript, and I think it's a good thing.

One of my favorite libraries is Angus Croll's just. If you look at the source for pretty much any of the modules in that library, what you'll find is straightforward, easy to read, easy to reason about code. It's nothing fancy, and that's what makes it so good.

I've been learning Go for the last month or so, and most of the code I've encountered is similar to Croll's JS code. It's almost always very easy to figure out what's happening. In fact, simplicity is one of the hallmarks of the language and that ethos of simplicity is reflected in the culture of people who write Go. Reading and writing in the language is starting to change my approach to JavaScript, and I think it's helping me to be a better coder.

I like JavaScript. It's the first language I learned to code with and I generally feel comfortable with all of its quirks. It's not the only language I'm familiar with. I just finished a CS degree where the primary language was C++ (shudder) and one of my biggest projects was written in Python. I even spent a couple of semesters writing Java. All of those languages can be written with simplicity and clarity in mind, but none of them are engineered for simplicity the way Go is.

While learning Go, I worked on a JS project and found myself adjusting my approach to how I write code and structure applications. Here are a couple of examples of how my habits are changing after learning a little Go.

Try/catch

Try/catch blocks help to guard against unexpected errors and exceptions. The idea is to try a potentially risky procedure, catch any errors that arise, and move on. It's a good idea in theory but I've never felt fully comfortable with using it. I always feel like my code gets trapped in the try portion of the try/catch block.

Here's a try/catch that'll be familiar to anyone who's coded JavaScript:

function getMyMomsFavoriteWebsite(siteUrl) {
  try {
    const response = await fetch(siteUrl);

    // Wait, there's a potential error when parsing the response
    // I should try/catch that as well
    try {
      const data = response.json();
      // dear lord, I'm in try/catch hell
    } catch (error) {
      // do something if an error pops up
      console.error("lazy error handling, etc", error);
    }
    // okay, both `response` and `data` live in this scope
    // and I can never leave...
  } catch (error) {
    // do something if an error pops up
    console.error("No, don't just log it to the console.", error);
  }
}

try {
  const response = await fetch(siteUrl);

  try {
    const data = await response.json();
    // help meeee!
  } catch (err) {
    console.error(err);
  }
} catch (err) {
  console.error(error);
}

There's another, simpler approach that's often taught in JS tutorials. I'm not really happy with it either.

The simple way (AKA: how they do it in tutorials)

Here's the thing about the usual fetch, ok, .json() flow of the next example — there's loads of potential for errors.

function getMyMomsFavoriteWebsite(siteUrl) {
  const response = await fetch(siteUrl);

  if (!response.ok) {
    console.error(`There was an error fetching the resource at ${siteUrl}`);
  }

  const data = await response.json();
  // okay, all sorts of errors can happen here...
  return data;
}

I prefer Go's error handling.

The Go way

// we need to define a type first (usually)
type FavoriteSite struct {
  Url string
  Title string
  Author string
  // ...etc.
}

func GetMyMomsFavoriteSite(siteUrl string) (*FavoriteSite, error) {
  client := http.Client{}
  fav := FavoriteSite{}
  res, err := client.Get("https://mymomsfavoritesite.com")
  if err != nil {
    // return zero-value FavoriteSite & error
    return fav, fmt.Errorf("error getting mom's favorite site %v", err)
    // okay, error handled, we can just move on
  }


  defer res.Body().Close()
  err := json.Unmarshal(res.Body, fav)

  if err != nil {
    return fav, fmt.Errorf("error parsing the response body", err)
  }
  
  return &fav, nil
}

One part of this code is a little more complicated — we need to know the shape of the data the response's json is going to give us. There are other ways to do it, but that's a normal approach. Notice the if err != nil part? In Go, errors are values, so you can check whether a function returned a nil error and respond accordingly. After getting used to handling errors the Go way, my approach to JS started to change. First, I started using try/catch differently.

Try/catch — new approach 1

function getMyMomsFavoriteSite(siteUrl) {
  // declare response here to use later
  let response;
  try {
    response = await fetch(siteUrl);
  } catch(error) {
    console.error(error);
    // we'll return something else in the next example
    return {};
  }

  let data;
  try {
    // oh, look! response is available outside it's try block!
    data = await response.json();
  } catch (error) {
    console.error(error);
    return {};
  }

  return data;
}

To me, the above example is a huge improvement over the approach taken to many fetch tutorials you'll see. Declaring the response and data variables outside the try/catch blocks means I have access to them outside of their block's scope. The try/catch block is now used strictly for exceptions and errors. But what about that return {}? I used an empty object in the example because the function is expected to return a JS object. I'm coming around to another approach that's inspired by Rust's Result.

Adding a result object

The Result in Rust is basically a way to return both the value that should be returned from a function and an indication of whether the function's operation was successful. This isn't really necessary in Go because you can define multiple return values, and a lot of functions will return a result and an error. There's no such thing in Rust. There's also no such thing in JavaScript. We can still use objects to represent the result of a function that might need to be an error.

It's as simple as:

return {ok: true, value: "Hey, I'm the valid result of a function!"};

Or:

return {ok: false, error: "Nope! Didn't work. Enjoy this cryptic message instead"};

The examples above don't take TypeScript into account, and there are several good examples of a Result type that ensures type safety. But I've been writing these examples in JS to keep this post simple. If you know and love TypeScript, feel free to adapt.

Here's the above example with object results:

function getMyMomsFavoriteWebsite(siteUrl) {
  let response;
  try {
    response = await fetch(siteUrl);
  } catch(err) {
    return {ok: false, error: `error fetching site at ${siteUrl}: ${err}`};
  }

  let data;
  try {
    data = await response.json();
  } catch(err) {
    return {ok: false, error: `error parsing response to json: ${err}`};
  }

  return {ok: true, value: data};
}

I haven't used the new approach for long, but it's already helping me think about how I approach functions, especially when they're within the lower layers of an application where I don't want to handle errors.

What do you think? Do you know of a better way to approach try/catch and error handling in JavaScript? Let me know at @[email protected].