Node.js
JavaScript
scripting
automation
coding tips

Run line at node script end?

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

In Node.js, "run something at the end of the script" can mean two different things: run code after your own logic finishes, or run code when the process is about to exit. Those cases are not the same, especially once asynchronous work enters the picture.

The Simplest Case: Put the Line Last

If your script is fully synchronous, the last line already runs at the end:

javascript
console.log("step 1");
console.log("step 2");
console.log("script finished");

That stops being enough as soon as you add file I/O, timers, database calls, or network requests. Node will keep the process alive while pending work exists, so the "end" of the file is not necessarily the end of the program.

Prefer try and finally Around Your Main Logic

If you control the script, the cleanest pattern is usually to wrap the main flow and use finally for cleanup.

javascript
1const fs = require("node:fs/promises");
2
3async function main() {
4  const file = await fs.readFile("input.txt", "utf8");
5  console.log(file.trim());
6}
7
8main()
9  .catch((error) => {
10    console.error("Script failed:", error);
11    process.exitCode = 1;
12  })
13  .finally(() => {
14    console.log("Cleanup complete");
15  });

This is usually what people actually want. The cleanup line runs after main resolves or rejects, and it still works with asynchronous code.

If you need to guarantee cleanup around part of the logic, try and finally inside main is even clearer:

javascript
1async function main() {
2  console.log("Opening resource");
3
4  try {
5    await new Promise((resolve) => setTimeout(resolve, 100));
6    console.log("Doing work");
7  } finally {
8    console.log("Closing resource");
9  }
10}
11
12main();

Use Process Events Only for Process-Level Behavior

Node also exposes lifecycle events on process, especially beforeExit and exit.

javascript
1process.on("beforeExit", (code) => {
2  console.log("beforeExit with code", code);
3});
4
5process.on("exit", (code) => {
6  console.log("exit with code", code);
7});
8
9console.log("Script body finished");

These events are useful, but they come with rules:

  • 'beforeExit fires when Node has no more work scheduled'
  • 'exit fires immediately before shutdown'
  • you should not expect asynchronous cleanup inside exit to complete

That last rule is critical. If you do this:

javascript
1process.on("exit", () => {
2  setTimeout(() => {
3    console.log("This will not run");
4  }, 10);
5});

the timer callback will never execute. The process is already exiting.

Handle Interruptions Explicitly

Sometimes "script end" really means "the user pressed Control+C" or the process received a termination signal. In that case, listen for signals rather than only relying on normal completion.

javascript
1process.on("SIGINT", () => {
2  console.log("Interrupted by user");
3  process.exit(130);
4});
5
6setInterval(() => {
7  console.log("Working...");
8}, 1000);

This is the right place for shutdown messages, closing open connections, or marking a job as interrupted.

Choose the Right Pattern

Use the last line of the file when everything is synchronous. Use finally when you want cleanup after a controlled operation. Use process.on("exit") only for small synchronous shutdown work. Use signal handlers for termination from outside the script.

That separation keeps the script predictable:

  • application logic owns its own cleanup
  • process events handle process lifecycle
  • signal handlers handle interruptions

Once you treat those as different tools, the confusion disappears.

Common Pitfalls

  • Assuming the last line of the file runs after asynchronous work. It only runs after the current synchronous turn.
  • Doing asynchronous work inside the exit handler. Node does not wait for it.
  • Calling process.exit() too early. That can terminate the process before logs flush or cleanup finishes.
  • Using process events when a local try and finally would be simpler and safer.
  • Forgetting about signals such as SIGINT when the real requirement is graceful shutdown on interruption.

Summary

  • In a synchronous script, the last line is the end.
  • In an asynchronous script, use try and finally or promise .finally() for cleanup.
  • 'beforeExit and exit are process lifecycle hooks, not a substitute for normal control flow.'
  • Keep exit handlers synchronous because asynchronous callbacks will not finish there.
  • If you need graceful shutdown on interruption, listen for signals such as SIGINT.

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.