Writing to files in Deno

Mayank C
Tech Tonic

--

Purpose

Writing to file is one of the basic operations that every application has to perform. Deno core runtime comes with useful functions to write data into a file. There are two ways to write:

  • The easy way: The easy way is to write a single string to a file present at a given path without worrying about anything else.
  • The efficient way: The longer but way more efficient way is to control the writing by opening and closing files ourselves.

The easy way is good for simple one-line writes, maybe at the time of startup, when efficiency isn’t of much concern. But if efficiency is of concern, then the easy way isn’t good. The efficient way gives more control to the user: when to open a file, when to write one or more times, and when to close a file.

In either of the cases, there is no need to import anything as the file writing functions are part of the core runtime.

First, let’s see the easy way, then we’ll see the efficient way.

Easy way

Deno’s core runtime has two useful functions for easy writing into files:

  • writeFile (sync variant — writeFileSync)
  • writeTextFile (sync variant — writeTextFileSync)

The difference is that writeFile function takes Uint8Array as input, while writeTextFile function takes a string as input. You can view writeFile as a function for writing binary data, while writeTextFile as a function for writing textual data. Though it’d be good to know that, writeTextFile uses writeFile internally after converting textual data to Uint8Array. In both of the cases, Deno takes care of opening and closing the file.

Both the writeFile and writeTextFile takes three inputs:

  • path: The path of the file to write to
  • data: The data to write to (string for writeTextFile and Uint8Array for writeFile)
  • options: These are the options to control the behavior of writing. This is an optional input.

Let’s see the options in detail:

  • append: Set this to true if data should be appended to the file. By default append is false.
  • create: Create the file if it doesn’t exist. By default create is true.
  • mode: Set permissions if a file is created

Here is an example of a simple write. The file /var/tmp/a.file doesn’t exist, so it gets created as well.

const str="TO WRITE INTO A FILE";await Deno.writeTextFile("/var/tmp/a.file", str);
//File is created and written
Deno.writeTextFileSync("/var/tmp/a.file", str);
//File is overwritten
// cat /var/tmp/a.file
// TO WRITE INTO A FILE

Sometimes the default behavior of overwriting the file isn’t useful. The options can be used to change it. We’ll see more examples later.

As mentioned earlier, this is the easy way, but not efficient. For every call of writeFile or writeTextFile, Deno opens the file, writes the data, and closes the file. If there are multiple such operations, it’d be getting very slow. For such cases, we need to go in an efficient way.

Efficient way

Depending on the use case, the more efficient way is to control the entire lifecycle, i.e. from open to close. Here is the typical lifecycle:

  • Open file: First, open the file
  • Write to file: Then, write to file as many times as needed
  • Close file: Last, close the file

Of course, the efficient way won’t be as sleek as the easy way. The efficient way is way more verbose, but it’d be very useful when there are multiple writes going to the same file.

Open file

First, open the file using the open (or openSync) function. This is a generic that can be used to open any resource. The open function returns a file object that would be used further.

const file=await Deno.open("/var/tmp/a.file");//orconst file=Deno.openSync(""/var/tmp/a.file");

Optionally, the open function takes the open options that are similar to the options that we saw for the writeFile function. The only difference is that the options for open are more granular than writeFile.

  • read: Open file for reading
  • write: Open file for writing
  • append: Open file to append data
  • truncate: Open file and delete all the existing data
  • create: Create a file if it doesn’t exist
  • createNew: Must create a new file, if exists fail
  • mode: The permissions to set if the file gets created
//create if doesn't exist, append if existsconst file=await Deno.open("/var/tmp/a.file", { create: true, append: true});

Write to file

The next step(s) is to write data into opened file. This step could be repeated as many times as needed. To write, there are two ways:

  • writeAll (or writeAllSync) function could be used. This is a generic buffer function that isn’t tied to a resource, therefore it needs a file object as its first argument. The second input to writeAll is the data to write. The data must be supplied in Uint8Array (text encoder is used to make the conversion).
await Deno.writeAll(file, new TextEncoder().encode(str));//orDeno.writeAllSync(file, new TextEncoder().encode(str));
  • file.write could also be used. This is almost the same as the writeAll function. Internally, functions writeAll and file.write calls the same write function.
await file.write(new TextEncoder().encode(str));// cat /var/tmp/a.file
// TO WRITE INTO A FILE

New lines, etc. need to be present in data. There is no formatting done by the low-level write functions.

Close file

Finally, when all the writing is done, the file or resource can be closed. Closing a resource releases memory, handles, etc. The close function is part of the file object.

file.close();

That’s all about the more efficient writing. It all depends on the use case. If a single string needs to be written once in a while, the easy way is the best. If the writing work is scattered, then the efficient way is the best.

Examples

Now that we’ve seen both the ways, let’s go over some examples.

First, simple file writes using writeFile or writeTextFile.

In this example, as no options are provided, the file is truncated for every write operation.

const str="TO WRITE INTO A FILE";await Deno.writeTextFile("/var/tmp/a.file", str);
Deno.writeTextFileSync("/var/tmp/a.file", str);
await Deno.writeFile("/var/tmp/a.file", new TextEncoder().encode(str));
Deno.writeFileSync("/var/tmp/a.file", new TextEncoder().encode(str));

In this example, an append option is provided.

await Deno.writeTextFile("/var/tmp/a.file", str, { append: true});
await Deno.writeFile("/var/tmp/a.file", new TextEncoder().encode(str), { append: true});
// cat /var/tmp/a.file
// TO WRITE INTO A FILETO WRITE INTO A FILE

In this example, create is explicitly set to false. As the file doesn’t exist, an error would be thrown:

await Deno.writeTextFile("/var/tmp/a.file", str, { append: true, create: false});//error: Uncaught (in promise) NotFound: No such file or directory (os error 2)

Next, multiple writes to a file using open, write, and close.

In this example, no options are provided, therefore an error is thrown as the file doesn’t exist:

const file=await Deno.open("/var/tmp/a.file");//error: Uncaught (in promise) NotFound: No such file or directory (os error 2)

In this example, create is provided, therefore a file is created and then opened. Note that the write option must be true if create is true. As there are no write operations, the file remains empty.

const file=await Deno.open("/var/tmp/a.file", { create: true, write: true});// ls -l /var/tmp/a.file
// -rw-r--r-- 1 mayankc wheel 0 Mar 20 12:49 /var/tmp/a.file

If the file exists, create option takes no effect. The file is directly opened.

const file=await Deno.open("/var/tmp/a.file", { create: true, write: true});// /var/tmp/a.file exists, no error is thrown

If the file must not exist, createNew option should be true. An error would be thrown if the file exists:

// /var/tmp/a.file existsconst file=await Deno.open("/var/tmp/a.file", { create: true, write: true});// error: Uncaught (in promise) AlreadyExists: File exists (os error 17)

Now, let’s go over the write operations:

// file /var/tmp/a.file is emptyconst file=await Deno.open("/var/tmp/a.file", { create: true, append: true});
await file.write(new TextEncoder().encode(str));
await Deno.writeAll(file, new TextEncoder().encode(str));
// cat /var/tmp/a.file
// TO WRITE INTO A FILETO WRITE INTO A FILE

The data is inserted as-is. If new lines are required, then they need to be part of the data.

const str="TO WRITE INTO A FILE\n";const file=await Deno.open("/var/tmp/a.file", { create: true, append: true});
file.writeSync(new TextEncoder().encode(str));
Deno.writeAllSync(file, new TextEncoder().encode(str));
// cat /var/tmp/a.file
// TO WRITE INTO A FILE
// TO WRITE INTO A FILE

Finally, the file needs to be closed. Real-world applications keep running, therefore it’s important to release the resources.

file.close();

Lastly, let’s see an example of everything together:

const str="TO WRITE INTO A FILE\n";
const filePath="/var/tmp/a.file";
await Deno.writeTextFile(filePath, str);const file1=await Deno.open(filePath, { create: true, write: true, truncate: true});
file1.writeSync(new TextEncoder().encode(str));
file1.close();
const file2=await Deno.open(filePath, { create: true, append: true});
await file2.write(new TextEncoder().encode(str));
await Deno.writeAll(file2, new TextEncoder().encode(str));
file2.close();
// cat /var/tmp/a.file
TO WRITE INTO A FILE
TO WRITE INTO A FILE
TO WRITE INTO A FILE

--

--