How Go Changed the Way We Handle Errors in JavaScript
If you’ve been dabbling with both Go (Golang) and JavaScript, you’ve probably come across a quirky-looking line like this:
const [data, err] = myFn();
At first glance, this might feel strange to JavaScript developers. But if you’ve spent some time with Go, this starts to make perfect sense. In fact, this pattern is inspired directly by Go’s way of handling errors — and it’s surprisingly useful in JavaScript, especially with async code.
Go’s Error Handling Philosophy
In Go, the idiomatic way to handle errors is:
data, err := myFunction()
if err != nil {
// handle the error
}
Go doesn't throw exceptions. Instead, most functions return two values: the result and an error. You then explicitly check whether the error is nil
or not. This approach encourages clear, predictable, and non-magical error handling.
The JavaScript Version of That Pattern
Since JavaScript supports array destructuring, we can mimic that Go-style error handling like this:
const [data, err] = myFn();
But here’s the trick — myFn()
needs to return an array with two values: [result, error]
.
Here’s an example implementation:
function myFn() {
try {
const result = doSomething(); // could throw
return [result, null];
} catch (e) {
return [null, e];
}
}
By returning a tuple [data, err]
, you gain full control over the flow. No unexpected exceptions, no mysterious stack traces — just plain, readable logic.
Taking It Further: Async/Await Version
This pattern becomes even more powerful with asynchronous code:
const [data, err] = await to(fetch('/api/user'));
To make that work, you’ll need a helper called to()
:
function to(promise) {
return promise
.then(data => [data, null])
.catch(err => [null, err]);
}
Now, instead of writing this:
try {
const res = await fetch('/api/user');
const data = await res.json();
} catch (e) {
console.error(e);
}
You can write this:
const [res, err] = await to(fetch('/api/user'));
if (err) {
console.error('Fetch failed:', err);
return;
}
const data = await res.json();
Much cleaner and easier to follow, especially when you’re dealing with multiple awaits in a row.
Why Developers Love This Pattern
- ✅ Explicit: No hidden errors — if
err
exists, you deal with it. - ✅ Readable: Keeps error logic close to the success logic.
- ✅ Avoids Try/Catch Hell: No nesting, no mental gymnastics.
- ✅ Testable: Much easier to mock or simulate errors.
A TypeScript Bonus (If You Care About Types)
Here’s a TypeScript-safe version of to()
:
function to<T>(promise: Promise<T>): Promise<[T, null] | [null, any]> {
return promise
.then(data => [data, null])
.catch(err => [null, err]);
}
It gives you proper type safety when destructuring [data, err]
.
Things to Consider
- ⚠️ Discipline required: You must always check for
err
. If you ignore it, bugs may creep in. - ⚠️ Not idiomatic JS: Not all developers are familiar with this pattern, so code reviews may need some context.
- ⚠️ Async-only context:
to()
works best when usingasync/await
. For regular functions, you’d still need try/catch or a wrapper.
Finally
This pattern may not be officially part of JavaScript, but it brings in the elegant discipline of Go into a language that often suffers from inconsistent error handling. If you’re building robust APIs, dealing with async flows, or simply want better readability, this Go-style approach can be a game changer.
It’s not just about style — it’s about writing more intentional, reliable JavaScript.
Comments ()