If you need to read or update SharePoint list data with plain JavaScript on a classic page, the JavaScript Object Model (JSOM) does it in three moves: get an SP.ClientContext, queue the work with load(), and send it with executeQueryAsync().
Where people get stuck is the setup. Most JSOM samples online are a decade old, they paste script into a Script Editor web part, and that web part isn’t even available on most SharePoint Online sites today.
In this tutorial, we’ll run real JSOM SharePoint examples on my SPGuides site: site properties, creating a list, adding, reading, updating and deleting items, and the current user’s groups. I’ll also show where JSOM still runs in 2026 and what to use instead on modern pages.
What is JSOM in SharePoint?
JSOM stands for JavaScript Object Model. It’s the browser version of the SharePoint client object model, shipped as SP.Runtime.js and SP.js under /_layouts/15/. It’s also called the ECMAScript object model.
It works like the C# client object model (CSOM): you build a batch of requests and nothing reaches the server until you execute it:
- SP.ClientContext is the entry point;
get_current()returns the current site’s context. - load() marks the objects and properties you want back, without calling the server.
- executeQueryAsync(success, failure) sends the whole batch in one request to
/_vti_bin/client.svc/ProcessQueryand runs one of your two callbacks.
That batching is why you can create a list, add two columns and read the result in one round trip.
Is JSOM still supported in SharePoint Online?
Yes, but only on classic pages. Microsoft’s JSOM reference says the classic JavaScript client object model isn’t supported with modern experiences or with the SharePoint Framework. In my tenant, modern list pages didn’t even load the SP object, while the classic Site contents page did.
Three more changes limit where it runs in 2026:
- Custom script is blocked by default on modern team and communication sites, which removes the Script Editor and Content Editor web parts. An admin can allow it per site, but SharePoint turns it back off within 24 hours.
- Content Security Policy has been enforced since March 1, 2026 (tenants could delay it to June 1, 2026). It blocks inline script on modern pages; Microsoft says it isn’t enforced on classic pages.
- SharePoint Add-ins, where most JSOM code used to live, stopped working on April 2, 2026.
So keep JSOM for maintaining classic pages, SharePoint Server farms and quick console scripts. For new work, use the SharePoint REST API or PnPjs inside SharePoint Framework solutions.
The sample lists we’ll use
Every read example uses my SPGDemo Orders list: 210 sales orders with Title (order number), Customer Name, Quantity, Order Total, Order Date and Order Status. A few rows have a blank total, which matters for the CAML query later.

For the write examples, Method 2 creates a second list, SPGDemo JSOM Tasks, with a TaskStatus choice column and a DueDate column. If you prefer building lists by hand, see creating a SharePoint list or library in SPFx for the modern way.
How to run JSOM code on a SharePoint page
I ran every example here from the browser console on the classic Site contents page (/_layouts/15/viewlsts.aspx), where SP.js is already loaded. No site settings change needed.
On a classic page you own, don’t call JSOM before SP.js is ready. SP.SOD.executeFunc loads it on demand and then runs your function:
SP.SOD.executeFunc("sp.js", "SP.ClientContext", function () {
var ctx = SP.ClientContext.get_current();
var web = ctx.get_web();
ctx.load(web, "Title");
ctx.executeQueryAsync(function () {
console.log("Connected to " + web.get_title());
}, function (sender, args) {
console.log("Request failed: " + args.get_message());
});
});
Save that code as a .js file in Site Assets and reference the file, because inline script is what CSP and the custom script setting block:
<div id="jsomResult"></div>
<script src="/sites/SPGuides/SiteAssets/jsom-demo.js"></script>
To see the results on the page, I use a small helper that writes into a panel at the top of the classic page. Every example below calls show() and onFail(). The HTML/JS idea is the same one a modern Script Editor web part built with SPFx uses:
// Writes the result into a panel at the top of the classic page's main area
function show(html) {
var panel = document.getElementById("jsomResult");
if (!panel) {
panel = document.createElement("div");
panel.id = "jsomResult";
panel.style.cssText = "display:inline-block;margin:0 0 16px;padding:12px 16px;border:2px solid #0078d4;" +
"background:#f3f9fd;font:15px 'Segoe UI';line-height:1.6";
var main = document.getElementById("DeltaPlaceHolderMain") || document.body;
main.prepend(panel);
}
panel.innerHTML = html;
}
function onFail(sender, args) {
show("Request failed: " + args.get_message());
}
Method 1: Get SharePoint site properties with JSOM
Start here to check which site you’re on. Naming the properties in load() keeps the response small:
var ctx = SP.ClientContext.get_current();
var web = ctx.get_web();
ctx.load(web, "Title", "ServerRelativeUrl", "Language", "WebTemplate");
ctx.executeQueryAsync(function () {
show("<b>Title:</b> " + web.get_title() +
"<br><b>Server-relative URL:</b> " + web.get_serverRelativeUrl() +
"<br><b>Language (LCID):</b> " + web.get_language() +
"<br><b>Template:</b> " + web.get_webTemplate());
}, onFail);
On my site it returned the title SPGuides, the server-relative URL /sites/SPGuides, language 1033 (English, United States) and the GROUP template of a group-connected team site.

How does this work?
get_web() only gives you an empty proxy object. The load(web, "Title", ...) call marks four properties to fetch, and executeQueryAsync sends the request.
In the success callback web.get_title() works. Read a property you didn’t load, like get_description(), and JSOM throws a “has not been initialized” error.
Method 2: Create a SharePoint list and add columns with JSOM
SP.ListCreationInformation describes the new list and addFieldAsXml adds its columns in the same batch:
var ctx = SP.ClientContext.get_current();
var info = new SP.ListCreationInformation();
info.set_title("SPGDemo JSOM Tasks");
info.set_templateType(SP.ListTemplateType.genericList);
info.set_description("Tasks created with the SharePoint JavaScript Object Model");
var list = ctx.get_web().get_lists().add(info);
// Two extra columns, added to the default view
var fields = list.get_fields();
fields.addFieldAsXml(
"<Field Type='Choice' DisplayName='TaskStatus' Name='TaskStatus'>" +
"<Default>Not Started</Default><CHOICES><CHOICE>Not Started</CHOICE>" +
"<CHOICE>In Progress</CHOICE><CHOICE>Completed</CHOICE></CHOICES></Field>",
true, SP.AddFieldOptions.addFieldInternalNameHint);
fields.addFieldAsXml(
"<Field Type='DateTime' DisplayName='DueDate' Name='DueDate' Format='DateOnly' />",
true, SP.AddFieldOptions.addFieldInternalNameHint);
ctx.load(list, "Title", "Id");
ctx.executeQueryAsync(function () {
show("Created list <b>" + list.get_title() + "</b> (ID " + list.get_id() + ")");
}, onFail);
The list appeared in Site contents with both columns in the default view.
How does this work?
SP.ListTemplateType.genericList is a plain custom list (template 100). The second argument of addFieldAsXml, true, adds the column to the default view.
addFieldInternalNameHint makes SharePoint use the Name from the XML as the internal name. That’s why I keep column names without spaces: TaskStatus stays TaskStatus instead of Task_x0020_Status.
Method 3: Add an item to a SharePoint list using JSOM
Adding an item is addItem, then set_item for each column, then update(). I’d already added four tasks the same way, so this one became item 5:
var ctx = SP.ClientContext.get_current();
var list = ctx.get_web().get_lists().getByTitle("SPGDemo JSOM Tasks");
var item = list.addItem(new SP.ListItemCreationInformation());
item.set_item("Title", "Order holiday gift cards");
item.set_item("TaskStatus", "Not Started");
// Noon UTC on 12/11/2026 (months are 0-based), so no time zone shifts the date
item.set_item("DueDate", new Date(Date.UTC(2026, 11, 11, 12)));
item.update();
ctx.load(item, "Id");
ctx.executeQueryAsync(function () {
show("Item created with ID <b>" + item.get_id() + "</b>");
}, onFail);
The panel showed “Item created with ID 5”, and the list view shows the new task with its 12/11/2026 due date.

How does this work?
An empty SP.ListItemCreationInformation creates a normal item in the root folder. Nothing is saved until executeQueryAsync runs, so get_id() only works in the success callback.
So here’s the catch with dates. My first run used new Date(2026, 10, 20) and SharePoint showed 11/19/2026: browser-time midnight is the evening before in my site’s Pacific time zone. Noon UTC avoids the shift.
Want the same thing without JSOM? Compare it with CRUD operations in an SPFx web part.
Method 4: Get SharePoint list items with a CAML query
Use getItems with an SP.CamlQuery whenever you need a filter, a sort or a row limit. This one returns the five largest orders that have a total:
var ctx = SP.ClientContext.get_current();
var list = ctx.get_web().get_lists().getByTitle("SPGDemo Orders");
var query = new SP.CamlQuery();
query.set_viewXml(
"<View><Query><Where>" +
"<IsNotNull><FieldRef Name='OrderTotal'/></IsNotNull>" +
"</Where><OrderBy><FieldRef Name='OrderTotal' Ascending='FALSE'/></OrderBy></Query>" +
"<ViewFields><FieldRef Name='Title'/><FieldRef Name='CustomerName'/>" +
"<FieldRef Name='OrderTotal'/><FieldRef Name='OrderStatus'/></ViewFields><RowLimit>5</RowLimit></View>");
var items = list.getItems(query);
ctx.load(items);
ctx.executeQueryAsync(function () {
var rows = "";
var e = items.getEnumerator();
while (e.moveNext()) {
var it = e.get_current();
rows += "<tr><td>" + it.get_item("Title") + "</td><td>" + it.get_item("CustomerName") +
"</td><td>" + it.get_item("OrderStatus") +
"</td><td>$" + it.get_item("OrderTotal").toFixed(2) + "</td></tr>";
}
show("<b>The 5 largest orders</b><table cellpadding='4'>" +
"<tr><th>Order</th><th>Customer</th><th>Status</th><th>Total</th></tr>" + rows + "</table>");
}, onFail);
SO-1102 for Adventure Works ($1,890.00) came back first, then Fabrikam ($1,499.88).

How does this work?
IsNotNull skips orders with a blank total. Without it, my first test crashed on toFixed() because a null total came back in the top five.
OrderBy sorts on the server, ViewFields limits the columns and RowLimit caps the batch. If writing CAML by hand feels error-prone, a CAML query builder can generate the XML for you.
Pro Tip: Always set a RowLimit. Lists with more than 5,000 items hit the list view threshold when your filter column isn’t indexed, and JSOM reports it as a failed query.
Method 5: Update a SharePoint list item using JSOM
To change an existing item, get it with getItemById, set the new values and call update(). Here’s item 1 moving to In Progress:
var ctx = SP.ClientContext.get_current();
var list = ctx.get_web().get_lists().getByTitle("SPGDemo JSOM Tasks");
var item = list.getItemById(1);
item.set_item("TaskStatus", "In Progress");
item.set_item("Title", "Prepare Q4 budget review (draft sent)");
item.update();
ctx.load(item, "Title", "TaskStatus");
ctx.executeQueryAsync(function () {
show("Item 1 is now <b>" + item.get_item("TaskStatus") + "</b>: " + item.get_item("Title"));
}, onFail);

How does this work?
getItemById(1) doesn’t fetch anything first. JSOM sends the update and the follow-up load in one request, so the panel shows the saved values: In Progress and the new title.
Method 6: Delete a SharePoint list item using JavaScript
JSOM gives you two ways to remove an item. I prefer recycle(), because it moves the item to the site recycle bin instead of deleting it for good:
var ctx = SP.ClientContext.get_current();
var list = ctx.get_web().get_lists().getByTitle("SPGDemo JSOM Tasks");
var item = list.getItemById(3);
item.recycle(); // deleteObject() would skip the recycle bin
ctx.executeQueryAsync(function () {
show("Item 3 moved to the site recycle bin");
}, onFail);
Item 3, Renew Adobe Acrobat licenses, landed in the recycle bin with its original location.

How does this work?
recycle() works like Delete in the browser, so users can restore the item for 93 days from the SharePoint recycle bin. deleteObject(), the method in most older samples, deletes the item permanently.
Method 7: Get the current user and their SharePoint groups
This is the classic way to show or hide page parts by group membership. get_currentUser() returns the signed-in user and get_groups() their SharePoint groups:
var ctx = SP.ClientContext.get_current();
var user = ctx.get_web().get_currentUser();
var groups = user.get_groups();
ctx.load(user, "Title", "IsSiteAdmin");
ctx.load(groups, "Include(Title)");
ctx.executeQueryAsync(function () {
var names = [];
var e = groups.getEnumerator();
while (e.moveNext()) { names.push(e.get_current().get_title()); }
show("<b>Signed in as:</b> " + user.get_title() +
"<br><b>Site collection admin:</b> " + user.get_isSiteAdmin() +
"<br><b>SharePoint groups:</b> " + (names.join(", ") || "none"));
}, onFail);

How does this work?
Include(Title) loads just the group names. My account showed as a site collection admin in three groups.
Keep in mind it lists SharePoint groups only. Access that comes from a Microsoft 365 group or admin rights won’t show up here. In SPFx you’d read the user from the page context instead, as in displaying the current user name in an SPFx web part.
Method 8: Use executeQueryAsync with async/await
Nested callbacks get messy fast. Wrap executeQueryAsync in a Promise and write JSOM top to bottom:
// Wrap executeQueryAsync in a Promise so you can use async/await
function executeQuery(ctx) {
return new Promise(function (resolve, reject) {
ctx.executeQueryAsync(resolve, function (sender, args) {
reject(new Error(args.get_message()));
});
});
}
async function countOrders() {
var ctx = SP.ClientContext.get_current();
var list = ctx.get_web().get_lists().getByTitle("SPGDemo Orders");
ctx.load(list, "Title", "ItemCount");
await executeQuery(ctx);
show(list.get_title() + " has <b>" + list.get_itemCount() + "</b> items");
}
countOrders().catch(function (err) { show("Request failed: " + err.message); });
It printed “SPGDemo Orders has 210 items”.
How does this work?
The wrapper resolves on success and rejects with the JSOM error message, so one catch handles every failure. Loading ItemCount is also the cheapest way to count list items, because it doesn’t read a single row.
JSOM vs REST API vs PnPjs: which should you use?
| Option | Where it runs | Use it when |
|---|---|---|
| JSOM | Classic pages, SharePoint Server, browser console | You maintain existing classic pages or need a quick console script |
| SharePoint REST API | Anywhere that can send HTTP requests | You need plain fetch calls, Power Automate or code outside SharePoint |
| PnPjs in SPFx | Modern pages, Teams, list extensions | You build anything new for SharePoint Online |
If you’re new to SPFx, building your first SharePoint client-side web part walks through the setup, and displaying list items in an SPFx web part is the modern version of Method 4.
Things to Keep in Mind
These are the errors and limits I hit most often with JSOM code, the first two in my own test runs. More SharePoint fixes live in our SharePoint tutorials hub.
- “List … does not exist”:
getByTitleneeds the exact display name, so check spelling and spaces. - “The property or field … has not been initialized”: add the property to
load()before you read it. - Large files: JSOM handles files up to 1.5 MB. Use the REST API for bigger uploads.
- Dates shift by a day: send dates as noon UTC, or convert to the site’s time zone first.
- Nothing happens on a modern page: there’s no
SP.jsthere. Move the code to an SPFx web part.
Moving JSOM code to modern SharePoint? These guides cover the next steps:
Frequently Asked Questions
What is the full form of JSOM in SharePoint?
JSOM stands for JavaScript Object Model, the browser version of the SharePoint client object model loaded from SP.Runtime.js and SP.js.
Is JSOM deprecated in SharePoint Online?
JSOM still works on classic pages, but Microsoft doesn’t support it on modern pages or in SPFx. The JavaScript library basic operations article carries that warning, and new projects should use REST or PnPjs in SPFx.
What does executeQueryAsync do in JSOM?
It sends every queued operation to SharePoint in one request, then calls your success or failure function. Loaded values exist only from the success callback on.
Can I use JSOM in a Script Editor web part?
Only on classic pages of sites that allow custom script, and SharePoint resets that setting within 24 hours. Inline script on modern pages is blocked by Content Security Policy in SharePoint Online.
What is the difference between JSOM and the REST API?
JSOM batches calls through client.svc and needs SP.js on the page. The REST API uses plain HTTP calls to /_api and works from any page, app or flow.

Hey! I’m Bijay Kumar, founder of SPGuides.com and a Microsoft Business Applications MVP (Power Automate, Power Apps). I launched this site in 2020 because I truly enjoy working with SharePoint, Power Platform, and SharePoint Framework (SPFx), and wanted to share that passion through step-by-step tutorials, guides, and training videos. My mission is to help you learn these technologies so you can utilize SharePoint, enhance productivity, and potentially build business solutions along the way.
it did not work for sharepoint online i am afraid
Hi Rodolfo,
The demo is itself in SharePoint Online. So it should be working fine at your end also. Are you getting any specific error?
I think he means it does not work in the modern experience.