June 18, 2018











In the previous article, we’ve talked about WebStorage API that you can nowadays use to store data in the browser. LocalStorage and sessionStorage were introduced in the HTML5. Does that mean that before we could not save data on the client side? Not at all – we’ve had (and still have) cookies. How to use document.cookie, and why would you want to do that now, when you have other possibilities? Let’s find out!

What are the cookies?

A cookie is a small piece of data that you can store in a browser. Before the introduction of mechanisms such as WebStorage API, they were the only way to store data on the browser. Therefore, cookies used to be a legitimate solution for general client-side storage.

It is a single string (up to 4KB) that combines all the data. After being stored in the browser, it can be accessed via document.cookie (with some exceptions, that we will cover today) and will contain key-value pairs separated by semicolons.

Operations on cookies through document.cookie

Property document.cookie is not just a regular string though: it is an object with a getter and a setter.

1 2 3 4 document . cookie = 'dog_name=fluffy' ; document . cookie = 'cat_name = garfield' ; console . log ( document . cookie ) ; //"dog_name=fluffy; cat_name=garfield"

As you can see, even if we assign a new value for document.cookie, it gets handled by the setter. The space around the assignment operator is not mandatory.

Since it is all stored in one string, you need to figure out a way to get just a single cookie. You can do it in many ways since it is just operating on a string:

1 2 3 4 5 6 7 8 9 function getCookie ( key ) { const regexp = new RegExp ( ` . * $ { key } = ( [ ^ ; ] * ) ` ) ; const result = regexp . exec ( document . cookie ) ; if ( result ) { return result [ 1 ] ; } } getCookie ( 'dog_name' ) ; // 'fluffy'

This approach uses template literals and regular expressions. If you don’t feel comfortable using RegExp, check out my regular expressions course:

Setting a cookie with the same key will cause it to be overwritten.

1 2 3 4 document . cookie = 'cat_name=garfield' ; document . cookie = 'cat_name=kitty' ; console . log ( document . cookie ) ; // cat_name=kitty;

document.cookie under the hood

You might feel tempted to retrieve the descriptor of document.cookie to check if it really contains getters and setters:

1 Object . getOwnPropertyDescriptor ( document , 'cookie' ) ; // undefined

It will return undefined because of the way that getOwnPropertyDescriptor works: it does not traverse the prototype chain.

A global variable document contains object actually inheriting from Document.prototype:

1 Document . prototype . isPrototypeOf ( document ) // true

and does not own a property called “cookie”, the Document.prototype does:

1 2 document . hasOwnProperty ( 'cookie' ) ; // false Document . prototype . hasOwnProperty ( 'cookie' ) ; // true

A possible way to retrieve the descriptor of document.cookie is to get the descriptor of Document.prototype.cookie itself:

1 Object . getOwnPropertyDescriptor ( Document . prototype , 'cookie' ) ;

There are deprecated functions called __lookupGetter__ and __lookupSetter__ that actually traverse the prototype chain and therefore, you can retrieve these methods by calling them on the document:

1 2 const cookieDescriptor = Object . getOwnPropertyDescriptor ( Document . prototype , 'cookie' ) ; cookieDescriptor . get === document . __lookupGetter__ ( 'cookie' ) // true

Set-Cookie HTTP response header

Using document.cookie is not an only way to set a cookie. An HTTP request might respond with a Set-Cookie header. As a result, a cookie will be sent by the browser of the client.

In Node.js you can do it with the setHeader function:

1 response . setHeader ( 'Set-Cookie' , [ '<cookie-name>=<cookie-value>' ] ) ;

It is a convenient way to, for example, handle authentication tokens. This is due to the fact, that you can set additional directives, which can make it all more secure.

This communication goes both ways: the browser will send your cookies with the requests that you make. It means, that if you would like that data to stay in the browser, it might be better to use Web Storage API.

Additional directives

You can put more than just simple string values for cookies. They can have additional parameters.

Expires

By default, cookies expire when the client closes. You can change this behaviour with the expires parameter. It is a maximum lifetime of the cookie.

1 2 3 const now = new Date ( ) ; now . setMinutes ( now . getMinutes ( ) + 1 ) ; document . cookie = ` cat_name = garfield ; expires = $ { now . toUTCString ( ) } ; ` ;

With the code above, the cookie will disappear after a minute.

An important thing to notice is that the additional parameters won’t show up in the document.cookie, but you can check it out in the Developer Tools in the Application tab. That means that you can’t check the expiry date through your code.

Setting an expiry date to now can be a convenient way to delete a cookie:

1 2 3 4 document . cookie = ` cat_name = garfield ; ` ; console . log ( document . cookie ) ; // 'cat_name=garfield;' document . cookie = ` cat_name = garfield ; expires = $ { ( new Date ( ) ) . toUTCString ( ) } ; ` ; console . log ( document . cookie ) ; // ''

Max-Age

Similar to expires but is a number of seconds till the cookie disappears. It has priority over expires.

Domain

It specifies what hosts can receive the cookie. If unspecified, it will default to the host of the current location (can be found in document.location), but will not include subdomains. If the domain is specified, the subdomains are always included.

Try going to https://www.mozilla.org and run this code in the devtools:

1 document . cookie = ` cat_name = garfield `

It will not be accessible on https://developer.mozilla.org.

But if you go again to https://www.mozilla.org and run this instead:

1 document . cookie = ` cat_name = garfield ; domain = mozilla . org `

it will be visible at https://developer.mozilla.org.

If the domain set on your cookie doesn’t match the URL that you send the request to, it won’t get appended to the request.

Path

It indicates a URL path that must exist in the requested URL.

If you go to https://www.reddit.com/r/javascript/ and type

1 document . cookie = ` cat_name = garfield ; path = javascript `

it will be available at https://www.reddit.com/r/javascript/new/, but not at https://www.reddit.com/.

If the path set on the cookie does not match the URL from the request that you are making, the cookie will not get attached to it.

Secure

A secure cookie will not be sent to the server until a request is made using SSL and the HTTPS protocol. Sites using HTTP can’t set cookies with the “secure” directive.

HttpOnly

This is a crucial directive. A cookie marked with HttpOnly will not be accessible through JavaScript and the document.cookie property. It makes it more secure and resistant to attacks like Cross-site scripting, or one of your dependencies being malicious. It can be set through the Set-Cookie response header.

Since browser sends cookies (with right Domain and Path) with requests, you can make use of it, making your application more secure.

Summary

Nowadays, we have better ways to store data in the browser, like Web Storage API. That does not make Cookies obsolete and you still should know how, and when to use them. A good example of that is saving the token in the cookies with the HttpOnly directive, making it more secure. I hope that this article helped you see the difference between cookies and more modern approaches to storing data in the browser.