This episode covers strategies for handling errors in Node.js: try/catch, error-first callbacks, error events from EventEmitter, and best practices for uncaughtException. You also learn debugging with the Node Inspector and DevTools.

Code that never errors only exists in imagination. What sets senior developers apart is how they handle errors: preventing crashes, recording useful traces, and fixing root causes quickly. Node.js has distinctive error patterns you need to recognize.
Episode 6 covers four layers of error handling in Node.js: try/catch for Promises, the error-first pattern for callbacks, the error event from EventEmitter, and global error handling. In the second half, we dive into debugging with Node Inspector and Chrome DevTools.
Ever since we adopted Promises (episode 5), try/catch has become the main tool:
import { readFile } from "node:fs/promises";
try {
const isi = await readFile("tidak-ada.txt", "utf8");
console.log(isi);
} catch (err) {
console.error("Gagal membaca:", err.code);
}When await produces an error, the flow jumps to the catch block and err contains details like code with the value ENOENT. Reading err.code is far more reliable than parsing text messages — use error codes for branching logic.
Legacy callback APIs in Node.js use the error-first convention: the callback's first argument is always an error, or null on success:
import { readFile } from "node:fs";
readFile("tidak-ada.txt", "utf8", (err, data) => {
if (err) {
console.error("Terjadi error:", err.message);
return;
}
console.log(data);
});The pattern (err, data) => ... forces you to check err first. This convention is still found in older packages, so understanding the pattern matters even though new projects use Promises.
Objects based on EventEmitter — such as streams, HTTP servers, and requests — handle errors through a special event named error. If there's no listener for this event, Node.js throws the error and stops the process:
import { EventEmitter } from "node:events";
const emitter = new EventEmitter();
emitter.on("error", (err) => {
console.error("Terjadi error:", err.message);
});
emitter.emit("error", new Error("koneksi gagal"));The pattern emitter.on("error", (err) => ...) catches the emitted error. Important rule: always attach an error listener to EventEmitter objects in your application, especially streams and servers, because an error without a listener will stop the entire process.
Two kinds of errors can escape local handling: an uncaught exception (uncaughtException) and a Promise that fails without catch (unhandledRejection). Both trigger global events:
process.on("uncaughtException", (err) => {
console.error("Uncaught exception:", err);
process.exit(1);
});
process.on("unhandledRejection", (reason) => {
console.error("Unhandled rejection:", reason);
});The best practice for uncaughtException: log the error, then exit. A process in an inconsistent state can no longer be trusted, and continuing to run is dangerous. Make sure there's a process manager that restarts the application — we'll discuss that in episodes 21 and 22.
Don't ignore exit codes. A successful script exits with code 0, while an error uses a non-zero code:
node app.js
echo "Exit code: $?"After node app.js, the $? variable in the shell holds the previous command's exit code. CI/CD uses this value to determine whether a build failed — we'll use it again in episode 18.
Node.js includes a built-in debugger based on the Chrome DevTools protocol. Run the application with the inspector flag, then open DevTools in the browser:
node --inspect app.jsnode --inspect app.js makes the process listen on port 9220 and displays the chrome://inspect URL. Open that page in Chrome, click your Node.js process, and you get a full panel: breakpoints, watch expressions, call stacks, and direct expression evaluation.
To stop at the program's first line, use --inspect-brk:
node --inspect-brk app.jsWith node --inspect-brk app.js, execution stops at the start so you can trace the code step by step. A browser-free alternative: node inspect app.js gives you a terminal debugger with commands like cont, next, step, and repl.
For simple problems, console.log remains the fastest tool. But distinguish the levels: console.log for info, console.warn for warnings, and console.error for errors. In episode 12 we'll replace them with structured, production-ready logging.
Here's what to take away:
try/catch for Promises and async/await.err argument first.error listener to EventEmitter objects.uncaughtException should be logged and then exit the process.--inspect and --inspect-brk open full debugging via Chrome DevTools.In the next episode, episode 7, we'll discuss building a simple HTTP server with the http module — createServer, reading the method and URL from a request, sending a response with status and headers, and testing the server with curl. This is the moment Node.js truly becomes a backend.