DEV Community

Cover image for JavaScript: Handling errors like Go
Eduardo Rabelo
Eduardo Rabelo

Posted on • Updated on

JavaScript: Handling errors like Go

Back in August, I wrote an article in Brazilian-Portuguese explaining how I use async/await to isolate error handling.

Today I'll translate it to English but with different examples!


I love how Go handle side-effects in a synchronous-like manner. Let's see this example from the net/http package:

func main() {
  res, err := http.Get("http://example.com/")

  if err != nil {
    // handle `err`
  }

  // do something with `res`
}
Enter fullscreen mode Exit fullscreen mode

Or perhaps the os package:

func main() {
  file, err := os.Open("words.txt")

  if err != nil {
    // handle `err`
  }

  // do something with `file`
}
Enter fullscreen mode Exit fullscreen mode

Implementation details aside, I was wondering if there's a way to write something like this in JavaScript?

Well, as they say, where there's a will, there's a way! 😂

Everyday Promise-like functions

Nowadays Promise-like environments are common amongst us.

We can use it to read a file in Node.js:

let util = require("util");
let fs = require("fs");

let read = util.promisify(fs.readFile);

function main() {
  read("./test.js", { encoding: "utf8" })
    .then(file => {
      // do something with `file`
    })
    .catch(err => {
      // handle `err`
    });
}

main();
Enter fullscreen mode Exit fullscreen mode

Perhaps fetching some data from an API:

let url = "https://dog.ceo/api/breeds/image/random";

function main() {
  fetch(url)
    .then(res => res.json())
    .then(res => {
      // do something with `res`
    })
    .catch(err => {
      // handle `err`
    });
}

main();
Enter fullscreen mode Exit fullscreen mode

And being lazy by nature, we create functions to hide some boilerplate for us, so we can write less code across the codebase:

let readFile = require("./readFile");

function main() {
  readFile("./test.js")
    .then(file => {
      // do something with `file`
    })
    .catch(err => {
      // handle `err`
    });
}

main();


// readFile.js
let util = require("util");
let fs = require("fs");

let read = util.promisify(fs.readFile);

module.exports = path => {
  return read(path, { encoding: "utf8" })
    .then(file => {
      return file;
    })
    .catch(err => {
      throw err;
    });
};
Enter fullscreen mode Exit fullscreen mode

And:

let api = require("./api");

function main() {
  api.getRandomDog()
    .then(res => {
      // do something with `res`
    })
    .catch(err => {
      // handle `err`
    });
}

main();


// api.js
let url = "https://dog.ceo/api/breeds/image/random";

let api = {};

api.getRandomDog = () => {
  return fetch(url)
    .then(res => res.json())
    .catch(err => {
      throw err;
    });
};

module.exports = api;
Enter fullscreen mode Exit fullscreen mode

Still, there's a lot of repetition here, there's .then and .catch in both sides of this code snippet.

They say async/await can fix this, so...let's try it then?

Converting to async/await

Let's see how our Node.js is doing in async/await:

let readFile = require("./readFile");

async function main() {
  try {
    let res = await readFile("./test.js");
    // do something with `file`
  } catch (err) {
    // handle `err`
  }
}

main();


// readFile.js
let util = require("util");
let fs = require("fs");

let read = util.promisify(fs.readFile);

module.exports = async path => {
  try {
    let res = await read(path, { encoding: "utf8" });
    return res;
  } catch (err) {
    throw err;
  }
};
Enter fullscreen mode Exit fullscreen mode

And how can we fetch our dog with it:

let api = require("./api");

async function main() {
  try {
    let res = await api.getRandomDog();
    // do something with `res`
  } catch (err) {
    // handle `err`
  }
}

main();

// api.js
let url = "https://dog.ceo/api/breeds/image/random";

let api = {};

api.getRandomDog = async () => {
  try {
    let res = await fetch(url);
    let json = await res.json();
    return json;
  } catch (err) {
    throw err;
  }
};

module.exports = api;
Enter fullscreen mode Exit fullscreen mode

Phew... I think we changed a problem by another. Now there's try...catch in both places. Thinking about our current interface between consumer/service, we've:

  1. In our main() function we're calling a "service" (readFile and api.)
  2. Our "service" function returns a Promise
  3. When fulfilled, our service returns a payload
  4. When rejected, our service throw an error

Hmm... perhaps this is the problem! Our interface between consumer/service are different for fulfilled and rejected scenarios.

Refreshing our memory about our Go example at the top:

 func main() {
  res, err := http.Get("http://example.com/")

  if err != nil {
    // handle `err`
  }

  // do something with `res`
}
Enter fullscreen mode Exit fullscreen mode

Seems we've the same interface for both, fulfilled and rejected scenario!

Let's try that with our last async/await example!

Unified return interface with async/await

In our Node.js example:

let readFile = require("./readFile");

async function main() {
  let [err, file] = await readFile("./test.js");

  if (err) {
    // handle `err`
  }

  // do something with `file`
}

main();


// readFile.js
let util = require("util");
let fs = require("fs");

let read = util.promisify(fs.readFile);

module.exports = async path => {
  try {
    let res = await read(path, { encoding: "utf8" });
    return [null, res];
  } catch (err) {
    return [err, null]
  }
};
Enter fullscreen mode Exit fullscreen mode

And our Fetch API:

let api = require("./api");

async function main() {
  let [err, res] = await api.getRandomDog();

  if (err) {
    // handle `err`
  }

  // do something with `res`
}

main();

// api.js
let url = "https://dog.ceo/api/breeds/image/random";

let api = {};

api.getRandomDog = async () => {
  try {
    let res = await fetch(url);
    let json = await res.json();
    return [null, json];
  } catch (err) {
    return [err, null]
  }
};

module.exports = api;
Enter fullscreen mode Exit fullscreen mode

Well done!! 🎉🎉🎉

That's exactly what we were looking for! Our main() function looks like our Go example and now we've isolated all try...catch in our "service" functions.

Using this approach you can clean up your Node.js Middlewares/Controllers and in your Front-end, let's say with React/Redux, clean up redux-thunks or redux-saga functions/generators.

You can also unit test these "service" functions in isolation and guarantee they are returning the expected interface/data.

Top comments (15)

Collapse
 
rhymes profile image
rhymes

Hi Eduardo, nice example but there are a couple of things that make me unconvinced this is the best approach in JavaScript:

  • JS wasn't designed to have a Go style of exceptions
  • in your example the error you pass on could be generated by .fetch() or by .json() and it's not going to be clear inside the main function
Collapse
 
oieduardorabelo profile image
Eduardo Rabelo

how are you rhymes? thanks for your comment!

JS wasn't designed to have a Go style of exceptions

not discussing the way the language handles side-effects (golang is built-in and javascript you need to wrap it in something like shown in this article), my personal experience is: it's just a matter of interface response pattern

i've being using it for node.js, typescript, flowtype, you name it... and it works pretty well, is just another pattern inside your codebase

in your example the error you pass on could be generated by .fetch() or by .json() and it's not going to be clear inside the main function

there is nothing preventing you to massage these responses... you can easily branch it in the catch:

api.getRandomDog = async () => {
  try {
    let res = await fetch(url);
    let json = await res.json();
    return [null, json];
  } catch (err) {
    if (err === "fetch-error") {
      return ["fetch-error", null]
    }
    if (err === "json-error") {
      return ["json-error", null]
    }
  }
};
Enter fullscreen mode Exit fullscreen mode

in the same way you can inspect the error in the main function:

async function main() {
  let [err, res] = await api.getRandomDog();

  if (err === "fetch-error") {
    // handle fetch error
  }

  if (err === "json-error") {
    // handle json error
  }

  // do something with `res`
}
Enter fullscreen mode Exit fullscreen mode

use your imagination, but be consistent! :) ... is rare when we need to have these granular error handling in the front-end... in my perspective, your back-end should be handling it (checking for missing params, not found user, credentials, etc)...

in an node.js/express world, your error middleware would be responsible for these branches and the client response/rejection could be only one!

many thanks 👋

Collapse
 
rhymes profile image
rhymes • Edited

how are you rhymes? thanks for your comment!

well thanks :D hope the same for you

i've being using it for node.js, typescript, flowtype, you name it... and it works pretty well, is just another pattern inside your codebase

Sure, I'm not against it, it's just that in a way you're limited to your perimeter of code, I wouldn't use it in a third party library. Most JS developers expect failure to be escalated to an exception so that if it's synchronous it can be caught and if it's async it can be handled. If suddenly you start returning two-valued arrays you're changing a silent contract. See how complicated it's going to be to change the way Go error handling is done. And Go is nowhere near as popular as JS.

It's not a bad idea inside an app though, as you say, if it's used consistently.

Thank you for your perspective!

Collapse
 
iaziz786 profile image
Mohammad Aziz • Edited

I have a question, even though this blog post is about Node.js, I have a question about Go.

How can you mimic Promise.all() in Go? What would be equivalent code in Go?

async function main() {
  // Assume all db calls will return a promise
  const firstUserPromise = firstDbCall().then((res) => res);
  const secondUserPromise = secondDbCall().then((res) => res);
  const thridUserPromise = thridDbCall().then((res) => res);

  const [
    firstUserData,
    secondUserData,
    thirdUserData
  ] = await Promise.all([firstUserPromise, secondUserPromise, thirdUserPromise]);
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
rhymes profile image
rhymes

You probably need to use a WaitGroup.

This is the example you can find in the documentation link:

package main

import (
    "sync"
)

type httpPkg struct{}

func (httpPkg) Get(url string) {}

var http httpPkg

func main() {
    var wg sync.WaitGroup
    var urls = []string{
        "http://www.golang.org/",
        "http://www.google.com/",
        "http://www.somestupidname.com/",
    }
    for _, url := range urls {
        // Increment the WaitGroup counter.
        wg.Add(1)
        // Launch a goroutine to fetch the URL.
        go func(url string) {
            // Decrement the counter when the goroutine completes.
            defer wg.Done()
            // Fetch the URL.
            http.Get(url)
        }(url)
    }
    // Wait for all HTTP fetches to complete.
    wg.Wait()
}
Collapse
 
iaziz786 profile image
Mohammad Aziz

I was not aware of WaitGroup. Thanks!

Collapse
 
foresthoffman profile image
Forest Hoffman

WaitGroup is awesome 🎉

Collapse
 
oieduardorabelo profile image
Eduardo Rabelo

funny enough, in Go is a little bit harder to do the exactly same!

i recommend you to go through two blog posts:

  1. Go: Concurrency Patterns: Pipelines and cancellation
  2. Go: Learn Concurrency

you will need to use channels/goroutines to achieve the same.. as said by rhymes in this thread, you will need to use WaitGroup!

there's a merge function in the documentation link of item 1 above, where using it, you can do something like this:

func makeRequest(url string) <-chan string {
    res := make(chan string)

    go func() {
        res <- "Request started..."

        _, err := http.Get(url)

        if err != nil {
            res <- "Failed."
        }

        res <- "Done!"
        close(res)
    }()

    return res
}

func main() {
    c1 := makeRequest("https://google.com/")
    c2 := makeRequest("https://twitter.com/")

    for n := range merge(c1, c2) {
        fmt.Println(n)
    }
}

running it will print:

$ go run main.go 
Request started...
Request started...
Done!
Done!

i've been using it since I discovered it :)

Collapse
 
theodesp profile image
Theofanis Despoudis

I think you can also use errgroup to handle errors gracefully

Collapse
 
iaziz786 profile image
Mohammad Aziz

This looks simple and powerful. Probably I'll have to dig deeper to understand it. Thanks for the resources.

Collapse
 
jeyj0 profile image
Jannis Jorre

Thanks for the post! I think this is a great pattern to follow.

I have one question left: is there a reason you return the error as first item and not the requested end-value?😇

Collapse
 
oieduardorabelo profile image
Eduardo Rabelo • Edited

thanks for your time Jannis! :)

my first approach was to mimic the Go way, but after it "clicked", I started using it in different ways!

🤗

Collapse
 
jeyj0 profile image
Jannis Jorre

Is there a reason though?🤔

Thread Thread
 
oieduardorabelo profile image
Eduardo Rabelo

the reason was to mimic Go... after switching between JavaScript and Go projects, I felt the need to try to write in a similar way in JavaScript :P

Thread Thread
 
jeyj0 profile image
Jannis Jorre

I got that - sorry if I wasn't clear enough, let me rephrase my question:

Is there a reason to go for a return that can be destructered as [err, value] over one that would be destructered with [value, err]?

I hope I made it understandable now.😇