Spec types: Completion, Property Descriptor & more
Read ECMAScript's bookkeeping types through small JavaScript examples: follow completions, inspect property flags, and recognize lists, closures, and bytes.
- 01Trace control flowRead normal and abrupt Completion Records to predict returns, throws, breaks, continues, and finally blocks.
- 02Inspect property rulesTell data and accessor descriptors apart, and test writable, enumerable, and configurable attributes.
- 03Read spec notationRecognize Lists, Records, Abstract Closures, and Data Blocks without mistaking them for JavaScript classes.
The spec has its own vocabulary
You can put JavaScript values in variables and log them. The ECMAScript specification also names intermediate values used to describe how an operation works. It calls these specification types. They are a way to explain behavior, not a new set of constructors for your application.
const cart = { total: 3 };
console.log(cart.total);Line 1 creates a cart with a total of 3. Line 2 reads that property and prints 3. A specification type is different: there is no JavaScript statement that hands you the actual Completion Record created while evaluating line 2.
A specification type is a meta-value used in ECMAScript algorithms. It can describe an intermediate result, but it cannot itself be stored as an ordinary JavaScript variable or object property.
In How to read ECMAScript, you learned to follow algorithm steps and double-square bracket names. Now we give those steps concrete names. This lesson covers Completion Records, Property Descriptors, Lists, Records, Abstract Closures, and Data Blocks. An Environment Record models name lookup in nested scopes; the later Environment Records lesson takes that topic further.
Completion Records: what happened next?
A Completion Record is the spec's account of a step's outcome. Its [[Type]] says whether control continues normally, returns, throws, breaks, or continues a loop. Its [[Value]] carries the resulting value. Its [[Target]] is a label for a directed break or continue, or the spec marker EMPTY when no label is needed.
const price = 3;
console.log(price);Line 1 stores the JavaScript value 3. Line 2 prints 3. The spec can describe that expression as completing normally with a value. The Completion Record is not another object printed to the console.
1. Return Completion Record { [[Type]]: NORMAL, [[Value]]: value, [[Target]]: EMPTY }.The quoted NormalCompletion step names all three fields. Here [[Type]] is NORMAL, [[Value]] is the produced value, and [[Target]] is EMPTY. EMPTY is a spec marker, not JavaScript undefined.
Replay instrumented JavaScript control flow. These frames illustrate Completion Records; they do not expose engine records.
script
function chooseTea(available) { try { if (!available) throw new Error("sold out"); return "tea"; } finally { console.log("checked"); }}The replay calls a real JavaScript function. Line 3 checks whether tea is available. Line 4 prepares a return of tea. Line 6 logs checked before the return reaches line 9, which prints tea. The output is therefore checked followed by tea. The colored frames replay instrumented example code; they do not inspect the engine.
| Type | Control flow | Example |
|---|---|---|
| NORMAL | Continue with a value | An expression finishes without transferring control |
| RETURN | Leave the current function | A return statement carries its value to the caller |
| THROW | Look for a handler | A throw statement carries the thrown value |
| BREAK / CONTINUE | Leave a loop / begin its next iteration | A label can be carried in [[Target]] |
You order food and receive a token. The token can tell you that your order is ready or that the item is sold out. The message and the order name answer different questions.
- In real life: Token says an order is ready
- In JavaScript: NORMAL means continue
- In real life: Token says an item sold out
- In JavaScript: THROW means leave for error handling
- In real life: Token includes the order name
- In JavaScript: [[Value]] carries the result
Where the analogy stops: A completion also models returns and labelled jumps; a real order token does not control a program.
Abrupt completions redirect control
An abrupt completion is any Completion Record whose type is not NORMAL. A throw moves toward a catch handler; a return leaves a function; break and continue direct loop evaluation. Abrupt does not mean something went wrong: a normal return statement is abrupt in this precise spec sense.
function priceOrError(price) { if (price < 0) throw new Error("negative price"); return price;}try { console.log(priceOrError(-1)); }catch (error) { console.log(error.message); }Line 1 defines a price check. Line 2 throws an Error when the price is negative, so line 3's return is skipped. Line 5 calls the function with -1; line 6 catches the thrown value and prints negative price.
The spec's Completion Record for that throw has type THROW and carries the Error as its value. The JavaScript Error is observable; the Completion Record wrapper is not. That distinction helps when an algorithm says "ReturnIfAbrupt" or uses the ? shorthand introduced in the previous lesson.
let served = 0;for (const order of ["tea", "skip", "done"]) { if (order === "skip") continue; if (order === "done") break; served++;}console.log(served);Line 1 starts the count at zero. Line 2 visits tea, skip, then done. Line 3 skips the second item; line 4 exits on the third. Line 5 counts only tea, so line 7 prints 1. When a jump has a label, [[Target]] carries that label; an ordinary unlabelled jump has no label to name.
Why finally can replace a return
A finally block runs even while a return or throw is pending. If finally finishes normally, that pending completion survives. If finally itself returns or throws, the new abrupt completion takes precedence. This is why a return from finally can hide an earlier result or even an error.
function getStatus() { try { return "ready"; } finally { return "closed"; }}console.log(getStatus());Line 1 defines getStatus. Line 2 prepares a return of ready. Line 3 returns closed from finally instead, so line 5 prints closed. The first return is not printed.
Compare the earlier tea replay: its finally block only logged checked, so tea still reached the caller. In real code, use finally for cleanup such as releasing a resource. Avoid returning from finally unless replacing a pending outcome is truly intended.
The same rule matters when try throws. A normal finally allows the throw to continue toward a handler; a throwing finally replaces it with the new thrown value. You can predict both cases by asking which completion finishes the finally block.
Property Descriptors describe own properties
A Property Descriptor is a spec Record describing an object's own property attributes. JavaScript lets you define those attributes with Object.defineProperty and inspect a regular object describing them with Object.getOwnPropertyDescriptor. The object returned by this API is not itself the internal spec Record.
const order = {};Object.defineProperty(order, "status", { value: "ready" });console.log(Object.getOwnPropertyDescriptor(order, "status").writable);console.log(Object.keys(order).length);Line 1 creates an empty order. Line 2 defines a status value but leaves flags out. Line 3 prints false for writable; line 4 prints 0 because enumerable also defaults to false on a new defined property. An ordinary property assigned with order.status = "ready" has different default flags.
1. If propertyDesc has a [[Value]] field, then
a. Perform CreateDataPropertyOrThrow(obj, "value", propertyDesc.[[Value]]).These short steps from FromPropertyDescriptor show the bridge: a spec field called [[Value]] becomes the ordinary object property value. The reverse direction, ToPropertyDescriptor, reads fields from a supplied JavaScript object.
Replay instrumented property creation and inspection. This is not an engine-internal trace.
script
Object.defineProperty(order, "status", { value: "ready", enumerable: false });const description = Object.getOwnPropertyDescriptor(order, "status");console.log(description.value, description.writable);console.log(Object.keys(order).length);The replay uses the lesson's real describeOrder function. The source defines status as ready and non-enumerable. It prints ready false on line 4, then 0 on line 5. The returned descriptor also reports configurable false. Back restores earlier snapshots without changing the created object in your app.
const order = {};Object.defineProperty(order, "status", { value: "ready", enumerable: true });console.log(Object.keys(order));statusreadytrueThe property still has value ready. Its enumerable flag is true, so Object.keys returns status.
Change only the enumerable checkbox. With it on, the output lists status. With it off, the list is empty but order.status stays ready. Reset restores true. This answers a common debugging question: a property can exist without appearing in Object.keys.
Data properties and accessor properties
A data descriptor has [[Value]] or [[Writable]]. An accessor descriptor has [[Getter]] or [[Setter]]. Both can carry enumerable and configurable fields. The spec forbids mixing data and accessor fields in a single descriptor.
const order = { price: 3 };Object.defineProperty(order, "total", { get() { return this.price; } });console.log(order.total);order.price = 4;console.log(order.total);Line 1 creates an order priced at 3. Line 2 installs a getter named total. Line 3 reads it and prints 3. Line 4 changes price to 4; line 5 calls the getter again and prints 4. The getter runs each time, rather than storing a separate total value.
| Field | Kind | Effect |
|---|---|---|
| value and writable | Data descriptor | Holds a value and controls ordinary assignment |
| get and set | Accessor descriptor | Runs functions when code reads or writes |
| enumerable | Either kind | Controls Object.keys and related enumeration |
| configurable | Either kind | Controls deletion and reconfiguration |
Writable controls assignment to a data property. The following example uses Reflect.set so a failed write produces a clear Boolean, regardless of whether an editor runs the snippet in strict mode.
const cart = {};
Object.defineProperty(cart, "total", { value: 3 });
console.log(Object.getOwnPropertyDescriptor(cart, "total").writable);
console.log(Reflect.set(cart, "total", 4), cart.total);Line 1 creates a cart. Line 2 defines total as 3. Line 3 prints false because writable was omitted. Line 4 prints false 3: the write fails and the stored value stays three.
const todo = {};
Object.defineProperty(todo, "done", { value: false, configurable: true });
console.log(delete todo.done);
console.log("done" in todo);Line 1 makes an empty todo. Line 2 defines done with configurable true. Line 3 prints true because deletion succeeds. Line 4 prints false because the property is gone. Without configurable true, deleting this newly defined property would fail.
const user = { name: "Asha" };
Object.defineProperty(user, "password", { value: "secret" });
console.log(Object.keys(user).join(","));
console.log(Object.getOwnPropertyDescriptor(user, "password").enumerable);Line 1 sets the user's name. Line 2 adds a password property but does not make it enumerable. Line 3 prints name, and line 4 prints false. Hiding a key from enumeration is not security: JavaScript can still read the value directly or inspect its descriptor.
Lists keep order; Records name fields
A List is an ordered sequence used inside specification algorithms, such as the argument values in a function call. Its notation uses angle quotes, for example « "tea", 3 ». This does not promise that the runtime created a JavaScript Array you can inspect.
function receipt(item, price) { console.log(item, price);}receipt("tea", 3);Line 1 declares receipt with item then price. Line 2 prints the two received values. Line 4 passes tea then 3, so the output is tea 3. The spec describes this ordered set of argument values as a List.
"« 1, 2 » defines a List value that has two elements"
"A new empty List can be expressed as « »."These short quotations are from the List language in the List and Record definitions. The same definition says argument lists use this ordered type. The function call above lets you see why preserving order matters.
A Record has named fields written with double square brackets in the spec. A Property Descriptor is one named Record shape. You can make a JavaScript object that looks similar, but that object is only a teaching comparison, not a spec Record value.
const result = { status: "ready", count: 3 };
console.log(result.status, result.count);Line 1 makes an ordinary object with named status and count properties. Line 2 prints ready 3. A spec Record similarly names fields, but its fields can contain other spec values and cannot be assigned to a JavaScript variable.
const label = { status: "ready" };
label.status = "sent";
console.log(label.status);Line 1 creates a real label. Line 2 updates its status. Line 3 prints sent. Do not interpret that update as changing the invisible spec Record that may have described an earlier operation.
| Type | Spec role | Do not confuse with |
|---|---|---|
| List | Ordered spec sequence | Arguments passed to a function, not necessarily a JS Array |
| Record | Named spec fields | A Property Descriptor is one kind of Record, not a JS plain object |
| Abstract Closure | Algorithm plus captured values | A spec helper, not a JavaScript function exposed to code |
| Data Block | Fixed-size mutable bytes | Explains ArrayBuffer storage, not the buffer object itself |
Abstract Closures capture values for an algorithm
An Abstract Closure is a spec helper: algorithm steps plus a collection of captured values. An algorithm creates it inline and later calls it like a function in spec notation. When the spec says it captures an alias, it takes the value associated with that alias at creation time.
function makeTax(adjustment) { return (price) => price + adjustment;}const addTax = makeTax(2);console.log(addTax(3));Line 1 defines a function that accepts an adjustment. Line 2 returns a function that adds it to a price. Line 4 calls the factory with 2; line 5 calls the result with 3 and prints 5. This JavaScript closure is an analogy for retaining information, not an Abstract Closure you can directly obtain.
1. Let addend be 41.
2. Let closure be a new Abstract Closure with parameters (x) that captures addend.
3. Let value be closure(1).The Abstract Closure definition uses an addend and then calls its helper. The displayed step 2 omits its nested arithmetic step. Captured aliases refer to their associated values when the helper is created; do not assume this rule applies to all JavaScript closure bindings.
let price = 3;
const savedPrice = price;
const showSaved = () => savedPrice;
price = 4;
console.log(showSaved(), price);Line 1 gives price the value 3. Line 2 copies that number into savedPrice. Line 3 builds a JavaScript function reading savedPrice. Line 4 changes price to 4. Line 5 prints 3 4. This is an explicit teaching comparison for capture-at-creation, not an implementation of the spec's Abstract Closure.
Data Blocks explain byte storage
A Data Block is a distinct, mutable sequence of fixed-length bytes in spec algorithms. Each byte is a number from 0 to 255. Creation initializes the bytes to zero. An ArrayBuffer is a JavaScript object associated with such storage; a typed array is a view that lets code read and write bytes.
const buffer = new ArrayBuffer(3);const bytes = new Uint8Array(buffer);console.log(bytes.join(","));bytes[1] = 255;console.log(bytes.join(","));Line 1 creates a buffer for three bytes; line 2 creates a byte view. Line 3 prints 0,0,0. Line 4 writes 255 to the second position, so line 5 prints 0,255,0. A view is what your code sees, not the specification's Data Block meta-value itself.
1. Let dataBlock be a new Data Block value consisting of size bytes.
2. Set all of the bytes of dataBlock to 0.
3. Return dataBlock.The short quotation is from CreateByteDataBlock. The zeroing step explains the first output. It does not claim the JavaScript constructor invokes that named helper in any particular engine implementation; the language promises the observable initial bytes.
const buffer = new ArrayBuffer(2);
const view = new DataView(buffer);
view.setUint16(0, 258);
console.log(view.getUint8(0), view.getUint8(1));Line 1 makes a two-byte buffer. Line 2 creates a DataView. Line 3 writes the number 258 as an unsigned 16-bit value using the default big-endian order. Line 4 prints 1 2 when reading the two bytes separately. Data Blocks explain the storage; the view specifies how a larger number becomes bytes.
The spec also defines Shared Data Blocks for storage accessible to more than one agent. Sharing brings additional memory-model rules; a plain ArrayBuffer example does not demonstrate cross-agent behavior. Keep that distinction whenever an algorithm mentions shared bytes.
Using spec vocabulary at work
Start with an observable symptom. A theme setting is readable in your application but missing from a list of keys. Before changing the interface, inspect its property descriptor: was enumerable set to false? Knowing the spec field tells you which JavaScript API can answer the question.
const settings = {};
Object.defineProperty(settings, "theme", { value: "light", enumerable: true });
console.log(Object.keys(settings).join(","));Line 1 starts an empty settings object. Line 2 defines a theme property with enumerable true. Line 3 prints theme. With enumerable false the key would vanish from Object.keys, although settings.theme would still have the value light. That is a practical reason to read a Property Descriptor.
For a handler that exits early, Completion Records help you follow a pending return through finally. For a function call, Lists help you track argument order. For binary data, Data Blocks help you distinguish the buffer from its views. Each concept answers a different debugging question; none requires you to construct a spec object.
[[Type]]: THROW[[Target]]of a labelled break[[Writable]]: false[[Getter]]- The List of call arguments
- The bytes underlying an ArrayBuffer
Place each clue with the type of behavior it describes, then read why the match fits.
Common misconceptions
It is tempting to turn every term in a spec algorithm into a JavaScript object. Resist that shortcut. The spec is describing required behavior, not mandating a particular engine representation of each intermediate meta-value.
const todo = {};
console.log(Object.getOwnPropertyDescriptor(todo, "done"));Line 1 creates an empty todo. Line 2 asks for its missing done property and prints undefined. That is an actual JavaScript result returned by the API; it is not the spec's marker EMPTY.
- "Every completion is an error." NORMAL is routine; RETURN is abrupt but not an error.
- "A descriptor is a plain object internally." The spec Record is different from the API's returned object.
- "Not enumerable means private." A property remains readable and inspectable.
- "EMPTY is undefined." EMPTY marks an absent spec value; undefined is a language value.
- "Spec Lists are JavaScript Arrays." A List is an abstract ordered sequence.
- "JS closures always snapshot." Abstract Closure capture rules do not rewrite JavaScript closure semantics.
| Term | What it is | Not the same as |
|---|---|---|
| Completion Record | A spec account of value and control flow | The object returned by a JavaScript function |
| Descriptor record | Internal specification fields | The ordinary JS object returned by getOwnPropertyDescriptor |
| EMPTY | A specification marker for no value or target | JavaScript undefined |
| Abstract Closure | Captures values at creation in spec algorithms | A guarantee every JS closure snapshots variables |
When reading a new spec algorithm, mark each term as either a language value you can observe or a meta-value explaining the transition. Then test a small related program. That keeps the model useful without pretending to inspect the engine directly.
Practice exercises
Predict each result before using the code editor. Try the input check, then open hints one at a time. The exercises move from reading a return to using descriptors in a website settings object.
A status function prepares a return, then runs a finally block. What single word appears in the console? Explain to yourself which return reaches the caller.
function status() { try { return "ready"; } finally { return "closed"; } }
console.log(status());function status() { try { return "ready"; } finally { return "closed"; } }
console.log(status());The finally return replaces the earlier ready return, so the code prints closed.
A cart defines total with only a value. What does its descriptor say for enumerable? Compare the answer with an ordinary property assignment after you finish.
const cart = {};
Object.defineProperty(cart, "total", { value: 3 });
console.log(Object.getOwnPropertyDescriptor(cart, "total").enumerable);const cart = {};
Object.defineProperty(cart, "total", { value: 3 });
console.log(Object.getOwnPropertyDescriptor(cart, "total").enumerable);Enumerable was omitted when total was defined, so the descriptor reports false.
A receipt builder accepts an item and price. Predict its output, including punctuation. Which argument belongs to the first parameter?
function receipt(item, price) { return item + ":" + price; }
console.log(receipt("tea", 3));function receipt(item, price) { return item + ":" + price; }
console.log(receipt("tea", 3));The ordered arguments fill item then price, producing tea:3.
This small JavaScript function retains an adjustment. What does the call print? Afterwards, explain why running this function does not expose a spec Abstract Closure.
const adjustment = 2;
const add = (price) => price + adjustment;
console.log(add(3));const adjustment = 2;
const add = (price) => price + adjustment;
console.log(add(3));The arrow function adds 2 to 3 and prints 5; it is still a JavaScript closure, not a spec Abstract Closure.
A fresh two-byte buffer backs a view. One byte changes. Type both bytes in the order printed and identify which part of the spec describes their initial values.
const bytes = new Uint8Array(new ArrayBuffer(2));
bytes[0] = 255;
console.log(bytes.join(","));const bytes = new Uint8Array(new ArrayBuffer(2));
bytes[0] = 255;
console.log(bytes.join(","));The first byte becomes 255 and the untouched second byte stays zero, so the output is 255,0.
A website lists visible settings with Object.keys. Its theme setting was defined explicitly. Which key does the list show? What flag would you change if that key had disappeared?
const settings = {};
Object.defineProperty(settings, "theme", { value: "light", enumerable: true });
console.log(Object.keys(settings).join(","));const settings = {};
Object.defineProperty(settings, "theme", { value: "light", enumerable: true });
console.log(Object.keys(settings).join(","));The theme key is enumerable, so Object.keys includes it and the program prints theme.
Check your understanding
Decide first whether the question is about control flow, property attributes, or an abstract sequence of values. Then predict any printed result from the code, not from a memorized slogan.
Question 1 of 8Which Completion Record type is not abrupt?
Choose an answer to see the explanation.
Question 2 of 8What does this finally example print?
Read the code, then predictfunction status() { try { return "ready"; } finally { return "closed"; } } console.log(status());Choose an answer to see the explanation.
Question 3 of 8What does [[Target]] describe?
Choose an answer to see the explanation.
Question 4 of 8What does this descriptor example print?
Read the code, then predictconst cart = {}; Object.defineProperty(cart, "total", { value: 3 }); console.log(Object.getOwnPropertyDescriptor(cart, "total").writable);Choose an answer to see the explanation.
Question 5 of 8Can one descriptor contain both value and get?
Choose an answer to see the explanation.
Question 6 of 8What does a spec List represent in a function call?
Choose an answer to see the explanation.
Question 7 of 8What do freshly created Data Block bytes start as?
Read the code, then predictconst bytes = new Uint8Array(new ArrayBuffer(2)); console.log(bytes.join(","));Choose an answer to see the explanation.
Question 8 of 8How does an Abstract Closure capture an alias in the spec?
Choose an answer to see the explanation.
For a missed question, return to its small runnable example above and change one input. This is especially useful for finally, the enumerable checkbox, and newly initialized bytes.
Key takeaways
The specification gives names to intermediate work so its algorithms are precise. Your JavaScript program observes the effects, not the meta-values themselves.
- Completion Records name NORMAL, BREAK, CONTINUE, RETURN, and THROW outcomes with value and target fields.
- Abrupt means control transfers; a return is abrupt without being an error.
- Property Descriptors explain own data or accessor properties and their writable, enumerable, and configurable attributes.
- Lists are ordered; Records have named fields. Neither implies a JavaScript Array or object you can store.
- Abstract Closures capture spec values; Data Blocks describe initialized byte storage.
- Test observable effects with real JavaScript when reading a spec algorithm.
Remember the one-liner.
Specification types describe how JavaScript behaves; your code works with the resulting language values.
Coming next: The Reference type & this. That lesson explains how property references give a method call its receiver. Afterward, Environment Records explain name lookup, and Abstract Operations connect the pieces.